diff --git a/.github/workflows/lua_linter.yaml b/.github/workflows/lua_linter.yaml
index d849e9d9b..008380e6f 100644
--- a/.github/workflows/lua_linter.yaml
+++ b/.github/workflows/lua_linter.yaml
@@ -2,9 +2,33 @@ name: lint
on:
pull_request:
- types: [opened, synchronize, reopened]
+ paths:
+ - "lua/**"
+ types: [opened, synchronize]
workflow_call:
+# partially taken from wiremod/wire/master/.github/workflows/lint.yml
jobs:
- lua-lint:
- uses: FPtje/GLuaFixer/.github/workflows/glualint.yml@master
\ No newline at end of file
+ lint:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@master
+
+ - name: Get any changed files
+ id: changed-files
+ uses: tj-actions/changed-files@v41
+ with:
+ files: |
+ **.lua
+
+ - name: Download GluaFixer
+ if: steps.changed-files.outputs.any_changed
+ run: |
+ curl -o glualint.zip -L https://github.com/FPtje/GLuaFixer/releases/download/1.28.0/glualint-1.28.0-x86_64-linux.zip
+ unzip glualint.zip
+
+ - name: Lint Code
+ if: steps.changed-files.outputs.any_changed
+ run: |
+ ./glualint ${{ steps.changed-files.outputs.all_changed_files }}
\ No newline at end of file
diff --git a/.github/workflows/update_workshop.yaml b/.github/workflows/update_workshop.yaml
index 86bd29e1e..f8501c44e 100644
--- a/.github/workflows/update_workshop.yaml
+++ b/.github/workflows/update_workshop.yaml
@@ -8,19 +8,15 @@ on:
workflow_dispatch:
jobs:
- linter:
- uses: CapsAdmin/pac3/.github/workflows/lua_linter.yaml@develop
-
update-workshop:
if: github.repository == 'CapsAdmin/pac3'
- # needs: linter
runs-on: ubuntu-latest
steps:
- name: Checkout
- uses: actions/checkout@v2
+ uses: actions/checkout@v4
- name: Publish to Steam Workshop
- uses: vurv78/gmod-upload@v0.1.3
+ uses: PAC3-Server/gmod-upload@master
env:
STEAM_USERNAME: ${{ secrets.STEAM_NAME }}
STEAM_PASSWORD: ${{ secrets.STEAM_PASSWORD }}
diff --git a/README.md b/README.md
index f5c5e2fbd..b36d0c200 100644
--- a/README.md
+++ b/README.md
@@ -16,4 +16,3 @@ Some links to check out:
* [discord server](https://discord.gg/utpR3gJ "Join PAC3 Discord Server")
---
-
diff --git a/lua/autorun/netstream.lua b/lua/autorun/netstream.lua
index bbbb2eace..588df6424 100644
--- a/lua/autorun/netstream.lua
+++ b/lua/autorun/netstream.lua
@@ -1,376 +1,386 @@
---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
- else
- pac.Message("net.Stream.ReadStream:Read(): hash received and hash of chunk do not match match")
- end
-
- if #self.chunks == self.numchunks then
- self.returndata = table.concat(self.chunks)
-
- if self.compressed then
- self.returndata = util.Decompress(self.returndata)
-
- if not self.returndata then
- pac.Message("net.Stream.ReadStream:Read(): Failed to decompress data")
- end
- 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!\n" .. debug.traceback() .. "\n")
- 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!\n" .. debug.traceback() .. "\n")
- 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)
+--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.SendSize = 20000 --This is the size of each packet to send
+net.Stream.Timeout = 30 --How long to wait for client response before cleaning up
+net.Stream.MaxWriteStreams = 1024 --The maximum number of write data items to store
+net.Stream.MaxReadStreams = 128 --The maximum number of queued read data items to store
+net.Stream.MaxChunks = 3200 --Maximum number of pieces the stream can send to the server. 64 MB
+net.Stream.MaxSize = net.Stream.SendSize*net.Stream.MaxChunks
+net.Stream.MaxTries = 3 --Maximum times the client may retry downloading the whole data
+
+local WriteStreamQueue = {
+ __index = {
+ Add = function(self, stream)
+ local identifier = self.curidentifier
+ local startid = identifier
+ while self.queue[identifier] do
+ identifier = identifier % net.Stream.MaxWriteStreams + 1
+ if identifier == startid then
+ ErrorNoHalt("Netstream is full of WriteStreams!")
+ net.WriteUInt(0, 32)
+ return
+ end
+ end
+ self.curidentifier = identifier % net.Stream.MaxWriteStreams + 1
+
+ if next(self.queue)==nil then
+ self.activitytimeout = CurTime()+net.Stream.Timeout
+ timer.Create("netstream_queueclean", 5, 0, function() self:Clean() end)
+ end
+ self.queue[identifier] = stream
+ stream.identifier = identifier
+ return stream
+ end,
+
+ Write = function(self, ply)
+ local identifier = net.ReadUInt(32)
+ local chunkidx = net.ReadUInt(32)
+ local stream = self.queue[identifier]
+ --print("Got request", identifier, chunkidx, stream)
+ if stream then
+ if stream:Write(ply, chunkidx) then
+ self.activitytimeout = CurTime()+net.Stream.Timeout
+ stream.timeout = CurTime()+net.Stream.Timeout
+ end
+ else
+ -- Tell them the stream doesn't exist
+ net.Start("NetStreamRead")
+ net.WriteUInt(identifier, 32)
+ net.WriteUInt(0, 32)
+ if SERVER then net.Send(ply) else net.SendToServer() end
+ end
+ end,
+
+ Clean = function(self)
+ local t = CurTime()
+ for k, stream in pairs(self.queue) do
+ if (next(stream.clients)~=nil and t >= stream.timeout) or t >= self.activitytimeout then
+ stream:Remove()
+ self.queue[k] = nil
+ end
+ end
+ if next(self.queue)==nil then
+ timer.Remove("netstream_queueclean")
+ end
+ end,
+ },
+ __call = function(t)
+ return setmetatable({
+ activitytimeout = CurTime()+net.Stream.Timeout,
+ curidentifier = 1,
+ queue = {}
+ }, t)
+ end
+}
+setmetatable(WriteStreamQueue, WriteStreamQueue)
+net.Stream.WriteStreams = WriteStreamQueue()
+
+local ReadStreamQueue = {
+ __index = {
+ Add = function(self, stream)
+ local queue = self.queues[stream.player]
+
+ if #queue == net.Stream.MaxReadStreams then
+ ErrorNoHalt("Receiving too many ReadStream requests!")
+ return
+ end
+
+ for _, v in ipairs(queue) do
+ if v.identifier == stream.identifier then
+ ErrorNoHalt("Tried to start a new ReadStream for an already existing stream!")
+ return
+ end
+ end
+
+ queue[#queue+1] = stream
+ if #queue == 1 then
+ stream:Request()
+ end
+ return stream
+ end,
+
+ Remove = function(self, stream)
+ local queue = rawget(self.queues, stream.player)
+ if queue then
+ if stream == queue[1] then
+ table.remove(queue, 1)
+ local nextInQueue = queue[1]
+ if nextInQueue then
+ nextInQueue:Request()
+ else
+ self.queues[stream.player] = nil
+ end
+ else
+ for k, v in ipairs(queue) do
+ if v == stream then
+ table.remove(queue, k)
+ break
+ end
+ end
+ end
+ end
+ end,
+
+ Read = function(self, ply)
+ local identifier = net.ReadUInt(32)
+ local queue = rawget(self.queues, ply)
+ if queue and queue[1] then
+ queue[1]:Read(identifier)
+ end
+ end
+ },
+ __call = function(t)
+ return setmetatable({
+ queues = setmetatable({}, {__index = function(t,k) local r={} t[k]=r return r end})
+ }, t)
+ end
+}
+setmetatable(ReadStreamQueue, ReadStreamQueue)
+net.Stream.ReadStreams = ReadStreamQueue()
+
+
+local WritingDataItem = {
+ __index = {
+ Write = function(self, ply, chunkidx)
+ local client = self.clients[ply]
+ if client.finished then return false end
+ if chunkidx == #self.chunks+1 then self:Finished(ply) return true end
+
+ if client.downloads+#self.chunks-client.progress >= net.Stream.MaxTries * #self.chunks then self:Finished(ply) return false end
+ client.downloads = client.downloads + 1
+
+ local chunk = self.chunks[chunkidx]
+ if not chunk then return false end
+
+ client.progress = chunkidx
+
+ --print("Sending", "NetStreamRead", self.identifier, #chunk.data, chunkidx, chunk.crc)
+ net.Start("NetStreamRead")
+ net.WriteUInt(self.identifier, 32)
+ net.WriteUInt(#chunk.data, 32)
+ net.WriteUInt(chunkidx, 32)
+ net.WriteString(chunk.crc)
+ net.WriteData(chunk.data, #chunk.data)
+ if CLIENT then net.SendToServer() else net.Send(ply) end
+ return true
+ end,
+
+ Finished = function(self, 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,
+
+ GetProgress = function(self, ply)
+ return self.clients[ply].progress / #self.chunks
+ end,
+
+ Remove = function(self)
+ local sendTo = {}
+ for ply, client in pairs(self.clients) do
+ if not client.finished then
+ client.finished = true
+ if CLIENT or ply:IsValid() then sendTo[#sendTo+1] = ply end
+ end
+ end
+
+ if next(sendTo)~=nil then
+ --print("Sending", "NetStreamRead", self.identifier, 0)
+ net.Start("NetStreamRead")
+ net.WriteUInt(self.identifier, 32)
+ net.WriteUInt(0, 32)
+ if SERVER then net.Send(sendTo) else net.SendToServer() end
+ end
+ end
+
+ },
+ __call = function(t, data, callback)
+ local chunks = {}
+ for i=1, math.ceil(#data / net.Stream.SendSize) 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
+
+ return setmetatable({
+ timeout = CurTime()+net.Stream.Timeout,
+ chunks = chunks,
+ callback = callback,
+ lasttouched = 0,
+ clients = setmetatable({},{__index = function(t,k)
+ local r = {
+ finished = false,
+ downloads = 0,
+ progress = 0,
+ } t[k]=r return r
+ end})
+ }, t)
+ end
+}
+setmetatable(WritingDataItem, WritingDataItem)
+
+local ReadingDataItem = {
+ __index = {
+ Request = function(self)
+ if self.downloads+self.numchunks-#self.chunks >= net.Stream.MaxTries*self.numchunks then self:Remove() return end
+ self.downloads = self.downloads + 1
+ timer.Create("NetStreamReadTimeout" .. self.identifier, net.Stream.Timeout*0.5, 1, function() self:Request() end)
+ self:WriteRequest()
+ end,
+
+ WriteRequest = function(self)
+ --print("Requesting", self.identifier, #self.chunks)
+ net.Start("NetStreamWrite")
+ net.WriteUInt(self.identifier, 32)
+ net.WriteUInt(#self.chunks+1, 32)
+ if CLIENT then net.SendToServer() else net.Send(self.player) end
+ end,
+
+ Read = function(self, identifier)
+ if self.identifier ~= identifier then self:Request() return end
+
+ local size = net.ReadUInt(32)
+ if size == 0 then self:Remove() return end
+
+ local chunkidx = net.ReadUInt(32)
+ if chunkidx ~= #self.chunks+1 then self:Request() return end
+
+ local crc = net.ReadString()
+ local data = net.ReadData(size)
+
+ if crc ~= util.CRC(data) then self:Request() return end
+
+ self.chunks[chunkidx] = data
+ if #self.chunks == self.numchunks then self:Remove(true) return end
+
+ self:Request()
+ end,
+
+ GetProgress = function(self)
+ return #self.chunks/self.numchunks
+ end,
+
+ Remove = function(self, finished)
+ timer.Remove("NetStreamReadTimeout" .. self.identifier)
+
+ local data
+ if finished then
+ data = table.concat(self.chunks)
+ if self.compressed then
+ data = util.Decompress(data, net.Stream.MaxSize)
+ end
+ self:WriteRequest() -- Notify we finished
+ end
+
+ local ok, err = xpcall(self.callback, debug.traceback, data)
+ if not ok then ErrorNoHalt(err) end
+
+ net.Stream.ReadStreams:Remove(self)
+ end
+ },
+ __call = function(t, ply, callback, numchunks, identifier, compressed)
+ return setmetatable({
+ identifier = identifier,
+ chunks = {},
+ compressed = compressed,
+ numchunks = numchunks,
+ callback = callback,
+ player = ply,
+ downloads = 0
+ }, t)
+ end
+}
+setmetatable(ReadingDataItem, ReadingDataItem)
+
+
+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
+
+ if #data > net.Stream.MaxSize then
+ ErrorNoHalt("net.WriteStream request is too large! ", #data/1048576, "MiB")
+ net.WriteUInt(0, 32)
+ return
+ end
+
+ local stream = net.Stream.WriteStreams:Add(WritingDataItem(data, callback, compressed))
+ if not stream then return end
+
+ --print("WriteStream", #stream.chunks, stream.identifier, compressed)
+ net.WriteUInt(#stream.chunks, 32)
+ net.WriteUInt(stream.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 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
+
+ local identifier = net.ReadUInt(32)
+ local compressed = net.ReadBool()
+
+ if numchunks > net.Stream.MaxChunks then
+ ErrorNoHalt("ReadStream requests from ", ply, " is too large! ", numchunks * net.Stream.SendSize / 1048576, "MiB")
+ return
+ end
+
+ --print("ReadStream", numchunks, identifier, compressed)
+
+ return net.Stream.ReadStreams:Add(ReadingDataItem(ply, callback, numchunks, identifier, compressed))
+end
+
+if SERVER then
+ util.AddNetworkString("NetStreamWrite")
+ util.AddNetworkString("NetStreamRead")
+end
+
+--Send requested stream data
+net.Receive("NetStreamWrite", function(len, ply)
+ net.Stream.WriteStreams:Write(ply or NULL)
+end)
+
+--Download the sent stream data
+net.Receive("NetStreamRead", function(len, ply)
+ net.Stream.ReadStreams:Read(ply or NULL)
+end)
diff --git a/lua/autorun/pac_restart.lua b/lua/autorun/pac_restart.lua
index 8c7e527b1..40eba4f1c 100644
--- a/lua/autorun/pac_restart.lua
+++ b/lua/autorun/pac_restart.lua
@@ -4,7 +4,7 @@ if SERVER then
return
end
-local sv_allowcslua = GetConVar('sv_allowcslua')
+local sv_allowcslua = GetConVar("sv_allowcslua")
local prefer_local_version = CreateClientConVar("pac_restart_prefer_local_version", "0")
function _G.pac_ReloadParts()
@@ -92,7 +92,7 @@ function _G.pac_Restart()
if pac and pac.Disable then
pacLocal.Message("removing all traces of pac3 from lua")
- pac.Disable()
+ pac.Disable(true)
pac.Panic()
if pace and pace.Editor then
@@ -110,7 +110,8 @@ function _G.pac_Restart()
for hook_name, hooks in pairs(hook.GetTable()) do
for id, func in pairs(hooks) do
- if isstring(id) and (id:StartWith("pace_") or id:StartWith("pac_") or id:StartWith("pac3_") or id:StartWith("pacx_")) then
+ local lower = isstring(id) and string.lower(id)
+ if lower and (lower:StartWith("pace_") or lower:StartWith("pac_") or lower:StartWith("pac3_") or lower:StartWith("pacx_")) then
hook.Remove(hook_name, id)
end
end
diff --git a/lua/autorun/pac_version.lua b/lua/autorun/pac_version.lua
index 6259e59ce..bb10abe9f 100644
--- a/lua/autorun/pac_version.lua
+++ b/lua/autorun/pac_version.lua
@@ -44,3 +44,292 @@ concommand.Add("pac_version", function()
end)
end
end)
+
+
+--accessed in the editor under pac-help-version
+function pac.OpenMOTD(mode)
+ local pnl = vgui.Create("DFrame")
+ pnl:SetSize(math.min(1400, ScrW()),math.min(900,ScrH()))
+
+ local html = vgui.Create("DHTML", pnl)
+
+ if mode == "combat_update" then
+ pnl:SetTitle("Welcome to a new update!")
+ html:SetHTML(pace.cedrics_combat_update_changelog_html)
+ elseif mode == "local_changelog" then
+ pnl:SetTitle("Latest changes of this installation")
+ html:SetHTML(pace.current_version_changelog_html)
+ elseif mode == "commit_history" then
+ pnl:SetTitle("Newest changes from the develop branch (please update your PAC version!)")
+ html:OpenURL("https://github.com/CapsAdmin/pac3/commits/develop")
+ end
+ html:Dock(FILL)
+
+ pnl:Center()
+ pnl:MakePopup()
+end
+
+
+--CHANGELOGS
+
+--one for the current install. edit as needed!
+pace.current_version_changelog_html = [[
+
+ Reminder: If you installed from github into your addons folder, you have to update manually
+ Major update special! : The Combat Update "PAC4.5"
+
+ new parts
+ damage zone
+ lock
+ force
+ health modifier
+ hitscan
+ interpolator
+
+ major editor customizability
+ keyboard shortcuts and new actions
+ part menu actions
+ part categories
+ favorites system and proxy/command banks
+ popup system (F1) and tutorials for parts and events
+ eventwheel colors
+
+ Bulk Select + Bulk Apply Properties
+
+ PAC Copilot
+ highlight an important property when creating some parts
+ re-focus to pac camera when creating camera part
+ setup command event if writing a generic name in an event
+ right click a material part's load VMT to gather the active model's materials for fast editing
+
+ Command event features
+ pac_event_sequenced command for activating series (combos, variations etc.)
+ eventwheel colors
+ eventwheel styles and grid eventwheel
+ pac_eventwheel_visibility_rule command for filtering eventwheel with many choices and keywords
+
+ More miscellaneous features
+ nearest life aim part names : point movable parts toward the nearest NPC or player
+ prompt for autoload : optional menu to pick between autoload.txt, your autosave backup, or your latest loaded outfit
+ improvements to player movement, physics, projectile parts
+
+
+ Changelog : November 2023
+ Fixed legacy lights
+
+ Changelog : October 2023
+ Fixed lights
+ Fixed webcontent limit cvar's help text
+ Add hook for autowear
+
+ Changelog : September 2023
+ Fix for .obj urls
+ Text part: define fonts, 2D text modes, more data sources
+ Updated README
+ prepare for automated develop deployment
+ keep original proxy data in .vmt files
+ fix player/weapon color proxies in mdlzips
+ add new material files when looking for missing ones
+
+ Changelog : August 2023
+ "spawn as props" bone scale retention
+ Fix/rework submitpart
+
+ Changelog : July 2023
+ small fix for sequential in legacy sound
+ less awkward selection when deleting keyframes
+ more proxy functions
+ Changed Hands SWEP to work better with pac
+ Reduce network usage on entity mutators
+ Various command part error handling
+
+ Changelog : June 2023
+ sequential playback for legacy sound and websound parts
+ more proxy functions
+
+ Changelog : June 2023
+ Fix new dropbox urls
+
+ Changelog : May 2023
+ make "free children" actually work
+ add a way to disable the eargrab animations
+
+ Changelog : April 2023
+ Fix voice volume error spam
+ Add input to proxy error
+ new hitpos pac bones
+ beam start/end width multipliers
+ text part features
+ a fix for singleplayer mute footsteps
+ options for particles
+ Prevent excessive pac size abuse
+
+ Changelog : February 2023
+ sort vmt directories
+ add support for 'patch' materials in mdlzips
+
+ Changelog : January 2023
+ fixing a small bug with autoload and button events
+ Added bearing to event and proxy to help with 2d sprites
+ maybe fix command events
+ add default values to command events
+ add cvars to hide pac cameras and/or 'in-editor' information
+
+]]
+
+
+--cedric's PAC4.5 combat update readme. please don't touch! it's a major update that happened once so it doesn't make sense to edit it after that
+pace.cedrics_combat_update_changelog_html = [[
+
+ PAC4.5
+
+ Welcome to my combat update for PAC3. Here's the overview of the important bits to expect.
+
+
+ damage_zone: deals damage (a more direct and controllable alternative to projectiles)
+
+ hitscan: shoots bullets
+
+ lock: teleport/grab
+
+ force: does physics forces
+
+ health_modifier: changes your health, armor etc
+
+ interpolated_multibone: morphs position / angles between different base_movables nodes, like a path
+
+ The combat features work with the principle of consent. The lock part especially is severely restricted for grabbing players, for what should be obvious reasons. You can only damage or grab players who have opted in for the corresponding consent.
+
+ pac_client_damage_zone_consent 0
+ pac_client_hitscan_consent 0
+ pac_client_force_consent 0
+ pac_client_grab_consent 0
+ pac_client_lock_camera_consent 0
+
+ There are also commands for clients to free themselves if they're being grabbed.
+
+ pac_break_lock
+ pac_stop_lock
+
+ Multiple options exist for servers to prevent mass abuse. cvars, size limits, damage limits, entity limits, which combat parts are allowed, as well as several net-protecting options to ease the load on the server's processing and on the network (reliable channel)...
+ I know how big of a change this is. When creating the settings the first time, the combat parts will only be enabled for singleplayer sandbox. In multiplayer and in other gamemodes, it will be 0.
+
+ pac_sv_combat_whitelisting 0
+ pac_sv_damage_zone 1
+ pac_sv_lock 1
+ pac_sv_lock_grab 1
+ pac_sv_lock_teleport 1
+ pac_sv_lock_max_grab_radius 200
+ pac_sv_combat_enforce_netrate 0
+ pac_sv_entity_limit_per_combat_operation 500
+ pac_sv_entity_limit_per_player_per_combat_operation 40
+ pac_sv_player_limit_as_fraction_to_drop_damage_zone 1
+ pac_sv_block_combat_features_on_next_restart 0
+ ...
+
+
+
+ Editor features:
+ Bulk Select
+ Select multiple parts and do some basic operations repeatedly. By default it's CTRL + click to select/unselect a part.
+ Along with it, Bulk Apply Properties is a new menu to change multiple parts' properties at once.
+ pac_bulk_select_halo_mode is a setting to decide when to highlight the bulk selected parts with the usual hover halos
+
+ Extensive customizability (user configs will be saved in data/pac3_config)
+ Customizable shortcuts for almost every action (in the pac settings menu).
+ Reordering the part menu actions layout (in the pac settings menu).
+ Changing your part categories, with possible custom icons.
+ Command events:
+ pac_eventwheel_visibility_rule
+ Colors for the event wheel (with a menu)
+ A new grid style and some sub-styles to choose from
+ Changeable activation modes between mouse click and releasing the bind
+ Visibility rules / filtering modes to filter out keywords etc. see the command pac_eventwheel_visibility_rule for more info.
+
+
+ Expanded settings menu
+ Clients can configure their editor experience, and owners with server access can configure serverwide combat-related limits and policies and more.
+ Favorite assets for quick access (user configs will be saved in data/pac3_config)
+ right click on assets in the pac asset browser to save it to your favorites. it can also try to do series if they end in a number, but it might fail. right clicking on the related field will bring it up in your list
+
+
+
+ A new framework to show information in the editor. Select a part and press F1 to open information about it. Currently holds part tutorials, part size and notes. It can be configured to be on various places, and different colors. Look for that in the editor's options tab.
+
+
+ PAC copilot : Foolproofing and editor assist
+ Selecting an event will pick an appropriate operator, and clicking away from a proxy without a variable name will notify you about how it won't work, telling you to go back and change it
+
+
+ Writing a name into an event's type will create a command event with that name if the name isn't a recognized event type, so you can quickly setup command events.
+
+
+ auto-disable editor camera to preview the camera part when creating a camera part
+
+
+ auto-focus on the relevant property when creating certain parts
+
+
+ Reference and help features
+ proxy bank: some presets with tooltip explanations. right click on the expression field to look at them
+
+
+ command bank: presets to use the command part. again, right click on the expression field to look at them
+
+
+ A built-in wiki written by me, for every part and most event types: short tooltips to tell you what a part does when you hover over the label when choosing which part to create, longer tutorials opened with F1 when you select an existing part.
+
+
+ Miscellaneous features
+ Part notes
+ a text field for the base_part, so you can write notes on any part.
+ Prompt for autoload
+ option to get a prompt to choose between your autoload file, your latest backup or latest loaded outfit when starting.
+ Queue prop/NPC outfits (singleplayer only)
+ option so that, when loading an outfit for props/NPCs, instead of hanging in the editor and needing to reassign the owner name manually, pac will not wear yet, but wait for you to spawn an appropriate prop or entity that had the outfit.
+
+
+ pac_event_sequenced
+ pac_event but with more options to control series of numbered events.
+ pac will try to register the max number when you create a command event with the relevant number e.g. to reach command10 you need to have a command event with the name command10. rewear for best results.
+ examples:
+ this increments by 1 (and loops back if necessary)
+ pac_event_sequenced hat_style +
+
+ this sets the series to 3
+ pac_event_sequenced hat_style set 3
+
+ keywords for going forward: +, add, forward, advance, sequence+
+ keywords for going backward: -, sub, backward, sequence-
+ keyword to set: set
+
+
+ Improvements to physics and projectile parts
+ Set the surface properties, preview the sizes and some more.
+ For projectiles to change the physics mesh (override mesh), it might have some issues.
+
+ Improvements to the player movement part
+ option to preserve in first person
+ An attempt to handle water, glass and ice better.
+
+
+ Bigger fonts for the editor + pac_editor_scale for the tree's scale
+ just a quick edit for people with higher resolution screens
+
+
+ New hover halos
+ You can recolor your hover halos for when you mouse over model parts and the bulk select
+ the default, pac_hover_color 255 255 255, is white, but you can change the R G B values or use the keywords rainbow, ocean, rave or none
+ pac_hover_pulserate controls the speed
+ pac_hover_halo_limit controls how many parts can be drawn. if there are too many, pac can break
+
+
+ -destroy hidden parts, proxies and events. I also call it Ultra cleanup. This is a quick but destructive optimization tool to improve framerate by only keeping visible parts and obliterating non-static elements. You can mark parts to keep by writing "important" in their notes field.
+ -Engrave targets: assign proxies and events' target part to quickly allow you to reorganize them in a separate group in the editor.
+ -dump model submaterials: same as dump player submaterials (prints the submaterials in the console) but for a pac3 model you select in the tree
+
+ Thank you for reading. Now go make something cool!
+ Yours truly,
+ Cédric.
+
+ ]]
diff --git a/lua/entities/gmod_wire_expression2/core/custom/pac.lua b/lua/entities/gmod_wire_expression2/core/custom/pac.lua
index b9890f78e..6f184a9a0 100644
--- a/lua/entities/gmod_wire_expression2/core/custom/pac.lua
+++ b/lua/entities/gmod_wire_expression2/core/custom/pac.lua
@@ -1,69 +1,91 @@
E2Lib.RegisterExtension("pac", true)
-util.AddNetworkString("pac_e2_setkeyvalue_str")
-util.AddNetworkString("pac_e2_setkeyvalue_num")
-util.AddNetworkString("pac_e2_setkeyvalue_vec")
-util.AddNetworkString("pac_e2_setkeyvalue_ang")
+util.AddNetworkString("pac_e2_setkeyvalue")
-local enabledConvar = CreateConVar("pac_e2_ratelimit_enable", "1", {FCVAR_ARCHIVE}, "If the e2 ratelimit should be enabled.", 0, 1)
-local rate = CreateConVar("pac_e2_ratelimit_refill", "0.025", {FCVAR_ARCHIVE}, "The speed at which the ratelimit buffer refills.", 0, 1000)
-local buffer = CreateConVar("pac_e2_ratelimit_buffer", "300", {FCVAR_ARCHIVE}, "How large the ratelimit buffer should be.", 0, 1000)
+local enabledConvar = CreateConVar("pac_e2_ratelimit_enable", "1", FCVAR_ARCHIVE, "If the e2 ratelimit should be enabled.", 0, 1)
+local rate = CreateConVar("pac_e2_ratelimit_refill", "0.025", FCVAR_ARCHIVE, "The amount at which the ratelimit buffer refills per second.", 0, 1000)
+local buffer = CreateConVar("pac_e2_ratelimit_buffer", "300", FCVAR_ARCHIVE, "How many PAC E2 operations are allowed before the rate limit is hit.", 0, 1000)
+local bytes = CreateConVar("pac_e2_bytelimit", "2048", FCVAR_ARCHIVE, "Limit number of bytes sent per second for PAC E2 messages.", 0, 65532)
+
+local byteLimits = WireLib.RegisterPlayerTable()
+local function canRunFunction(self, g, k, v)
+ local byteLimit = byteLimits[self.player]
+ local ct = CurTime()
+ if not byteLimit then
+ byteLimit = { ct + 1, 0 }
+ byteLimits[self.player] = byteLimit
+ end
+
+ local lim = #g + #k + #v
+ if ct < byteLimit[1] then
+ lim = lim + byteLimit[2]
+ else
+ byteLimit[1] = ct + 1
+ end
+ byteLimit[2] = lim
+
+ if lim >= bytes:GetInt() then return self:throw("pac3 e2 byte limit exceeded", false) end
-local function canRunFunction(self)
if not enabledConvar:GetBool() then return true end
- local allowed = pac.RatelimitPlayer(self.player, "e2_extension", buffer:GetInt(), rate:GetInt())
+ local allowed = pac.RatelimitPlayer(self.player, "e2_extension", buffer:GetInt(), rate:GetFloat())
if not allowed then
- E2Lib.raiseException("pac3 e2 ratelimit exceeded")
- return false
+ return self:throw("pac3 e2 ratelimit exceeded", false)
end
return true
end
+--- Domain-specific type IDs for networking E2 keyvalues
+---@alias pac.E2.NetID
+---| 0 # String
+---| 1 # Number
+---| 2 # Vector
+---| 3 # Angle
+
e2function void pacSetKeyValue(entity owner, string global_id, string key, string value)
- if not canRunFunction(self) then return end
- net.Start("pac_e2_setkeyvalue_str")
+ if not canRunFunction(self, global_id, key, value) then return end
+ net.Start("pac_e2_setkeyvalue", true)
net.WriteEntity(self.player)
net.WriteEntity(owner)
net.WriteString(global_id)
net.WriteString(key)
-
+ net.WriteUInt(0, 2)
net.WriteString(value)
net.Broadcast()
end
e2function void pacSetKeyValue(entity owner, string global_id, string key, number value)
- if not canRunFunction(self) then return end
- net.Start("pac_e2_setkeyvalue_num")
+ if not canRunFunction(self, global_id, key, "nmbr") then return end -- Workaround because I don't want to add cases for each type, 4 bytes
+ net.Start("pac_e2_setkeyvalue", true)
net.WriteEntity(self.player)
net.WriteEntity(owner)
net.WriteString(global_id)
net.WriteString(key)
-
+ net.WriteUInt(1, 2)
net.WriteFloat(value)
net.Broadcast()
end
e2function void pacSetKeyValue(entity owner, string global_id, string key, vector value)
- if not canRunFunction(self) then return end
- net.Start("pac_e2_setkeyvalue_vec")
+ if not canRunFunction(self, global_id, key, "vctrvctrvctr") then return end -- 4 bytes, 3 times
+ net.Start("pac_e2_setkeyvalue", true)
net.WriteEntity(self.player)
net.WriteEntity(owner)
net.WriteString(global_id)
net.WriteString(key)
-
- net.WriteVector(Vector(value[1], value[2], value[3]))
+ net.WriteUInt(2, 2)
+ net.WriteVector(value)
net.Broadcast()
end
e2function void pacSetKeyValue(entity owner, string global_id, string key, angle value)
- if not canRunFunction(self) then return end
- net.Start("pac_e2_setkeyvalue_ang")
+ if not canRunFunction(self, global_id, key, "vctrvctrvctr") then return end
+ net.Start("pac_e2_setkeyvalue", true)
net.WriteEntity(self.player)
net.WriteEntity(owner)
net.WriteString(global_id)
net.WriteString(key)
-
- net.WriteAngle(Angle(value[1], value[2], value[3]))
+ net.WriteUInt(3, 2)
+ net.WriteAngle(value)
net.Broadcast()
end
diff --git a/lua/pac3/core/client/base_movable.lua b/lua/pac3/core/client/base_movable.lua
index f9886c30d..aa81d9112 100644
--- a/lua/pac3/core/client/base_movable.lua
+++ b/lua/pac3/core/client/base_movable.lua
@@ -24,6 +24,9 @@ BUILDER
["player eyes"] = "PLAYEREYES",
["local eyes yaw"] = "LOCALEYES_YAW",
["local eyes pitch"] = "LOCALEYES_PITCH",
+ ["nearest npc or player (torso-level)"] = "NEAREST_LIFE",
+ ["nearest npc or player (entity position)"] = "NEAREST_LIFE_POS",
+ ["nearest npc or player (yaw only)"] = "NEAREST_LIFE_YAW"
}})
:GetSetPart("Parent")
:EndStorableVars()
@@ -37,7 +40,7 @@ do -- bones
function PART:GetBonePosition()
local parent = self:GetParent()
if parent:IsValid() then
- if parent.ClassName == "jiggle" then
+ if parent.ClassName == "jiggle" or parent.ClassName == "interpolated_multibone" then
return parent.pos, parent.ang
elseif
not parent.is_model_part and
@@ -61,11 +64,13 @@ do -- bones
function PART:GetBoneMatrix()
local parent = self:GetParent()
- if parent:IsValid() then
- if parent.ClassName == "jiggle" then
+ if IsValid(parent) then
+ if parent.ClassName == "jiggle" or parent.ClassName == "interpolated_multibone" then
local bone_matrix = Matrix()
- bone_matrix:SetTranslation(parent.pos)
- bone_matrix:SetAngles(parent.ang)
+ if parent.pos then
+ bone_matrix:SetTranslation(parent.pos)
+ bone_matrix:SetAngles(parent.ang)
+ end
return bone_matrix
elseif
not parent.is_model_part and
@@ -185,6 +190,43 @@ function PART:CalcAngles(ang, wpos)
return self.Angles + (pac.EyePos - wpos):Angle()
end
+ local function get_nearest_ent(part)
+ local nearest_ent = part:GetRootPart():GetOwner()
+ local nearest_dist = math.huge
+ local owner_ent = part:GetRootPart():GetOwner()
+
+ for _,ent in pairs(ents.FindInSphere(wpos, 5000)) do
+ if (ent:IsNPC() or ent:IsPlayer()) and ent ~= owner_ent then
+ local dist = (wpos - ent:GetPos()):LengthSqr()
+ if dist < nearest_dist then
+ nearest_ent = ent
+ nearest_dist = dist
+ end
+ end
+ end
+
+ return nearest_ent
+ end
+
+ if pac.StringFind(self.AimPartName, "NEAREST_LIFE_YAW", true, true) then
+ local nearest_ent = get_nearest_ent(self)
+ if not IsValid(nearest_ent) then return ang or Angle(0,0,0) end
+ local ang = (nearest_ent:GetPos() - wpos):Angle()
+ return Angle(0,ang.y,0) + self.Angles
+ end
+
+ if pac.StringFind(self.AimPartName, "NEAREST_LIFE_POS", true, true) then
+ local nearest_ent = get_nearest_ent(self)
+ if not IsValid(nearest_ent) then return ang or Angle(0,0,0) end
+ return self.Angles + (nearest_ent:GetPos() - wpos):Angle()
+ end
+
+ if pac.StringFind(self.AimPartName, "NEAREST_LIFE", true, true) then
+ local nearest_ent = get_nearest_ent(self)
+ if not IsValid(nearest_ent) then return ang or Angle(0,0,0) end
+ return self.Angles + ( nearest_ent:GetPos() + Vector(0,0,(nearest_ent:WorldSpaceCenter() - nearest_ent:GetPos()).z * 1.5) - wpos):Angle()
+ end
+
if self.AimPart:IsValid() and self.AimPart.GetWorldPosition then
return self.Angles + (self.AimPart:GetWorldPosition() - wpos):Angle()
end
diff --git a/lua/pac3/core/client/base_part.lua b/lua/pac3/core/client/base_part.lua
index 153594701..520e47d97 100644
--- a/lua/pac3/core/client/base_part.lua
+++ b/lua/pac3/core/client/base_part.lua
@@ -31,6 +31,7 @@ BUILDER
:StartStorableVars()
:SetPropertyGroup("generic")
:GetSet("Name", "")
+ :GetSet("Notes", "", {editor_panel = "generic_multiline"})
:GetSet("Hide", false)
:GetSet("EditorExpand", false, {hidden = true})
:GetSet("UniqueID", "", {hidden = true})
@@ -53,6 +54,7 @@ function PART:IsValid()
end
function PART:PreInitialize()
+ if pace == nil then pace = _G.pace end --I found that it is localized before pace was created
self.Children = {}
self.ChildrenMap = {}
self.modifiers = {}
@@ -141,6 +143,18 @@ function PART:SetUniqueID(id)
end
end
+function PART:SetNotes(str)
+ self.Notes = str
+ if self:GetPlayerOwner() ~= pac.LocalPlayer then return end
+ if self.pace_tree_node and self.pace_tree_node.Label then
+ if str ~= "" then
+ self.pace_tree_node.Label:SetTooltip(str)
+ else
+ self.pace_tree_node.Label:SetTooltip()
+ end
+ end
+end
+
local function set_info(msg, info_type)
if not msg then return nil end
local msg = tostring(msg)
@@ -202,10 +216,12 @@ do -- owner
end
function PART:GetParentOwner()
+
if self.TargetEntity:IsValid() and self.TargetEntity ~= self then
return self.TargetEntity:GetOwner()
end
+
for _, parent in ipairs(self:GetParentList()) do
-- legacy behavior
@@ -561,6 +577,164 @@ do -- scene graph
end
end
+ function PART:SetSmallIcon(str)
+ if str == "event" then str = "icon16/clock_red.png" end
+ if self.pace_tree_node then
+ if self.pace_tree_node.Icon then
+ if not self.pace_tree_node.Icon.event_icon then
+ local pnl = vgui.Create("DImage", self.pace_tree_node.Icon)
+ self.pace_tree_node.Icon.event_icon_alt = true
+ self.pace_tree_node.Icon.event_icon = pnl
+ pnl:SetSize(8*(1 + 0.5*(GetConVar("pac_editor_scale"):GetFloat()-1)), 8*(1 + 0.5*(GetConVar("pac_editor_scale"):GetFloat()-1)))
+ pnl:SetPos(8*(1 + 0.5*(GetConVar("pac_editor_scale"):GetFloat()-1)), 8*(1 + 0.5*(GetConVar("pac_editor_scale"):GetFloat()-1)))
+ end
+ self.pace_tree_node.Icon.event_icon_alt = true
+ self.pace_tree_node.Icon.event_icon:SetImage(str)
+ self.pace_tree_node.Icon.event_icon:SetVisible(true)
+ end
+ end
+ end
+ function PART:RemoveSmallIcon()
+ if self.pace_tree_node then
+ if self.pace_tree_node.Icon then
+ if self.pace_tree_node.Icon.event_icon then
+ self.pace_tree_node.Icon.event_icon_alt = false
+ self.pace_tree_node.Icon.event_icon:SetImage("icon16/clock_red.png")
+ self.pace_tree_node.Icon.event_icon:SetVisible(false)
+ end
+ end
+ end
+ end
+
+ local pac_doubleclick_type = CreateClientConVar("pac_doubleclick_action", "expand", true, true, "What function should be run if you double-click on a part node.\n\nexpand : expand or collapse the node\nrename : rename the part\nnotes : write notes for the part\nshowhide : shows or hides the part\nspecific_only : only trigger class-specific actions, such as playing sounds, triggering hitscans, etc.\nnone : disable double-click actions")
+
+ function PART:OnDoubleClickBaseClass()
+ pace.doubleclickfunc = pac_doubleclick_type:GetString()
+ if pace.doubleclickfunc == "specific_only" then return end
+
+ pace.FlashNotification("double click action : " .. pace.doubleclickfunc)
+
+ if pace.doubleclickfunc == "expand" then
+ if not self:HasChildren() then return end
+ self:SetEditorExpand(not self:GetEditorExpand())
+ pace.RefreshTree()
+ elseif pace.doubleclickfunc == "rename" then
+ local pnl = vgui.Create("DTextEntry")
+ pnl:SetFont(pace.CurrentFont)
+ pnl:SetDrawBackground(false)
+ pnl:SetDrawBorder(false)
+ pnl:SetText(self:GetName())
+ pnl:SetKeyboardInputEnabled(true)
+ pnl:RequestFocus()
+ pnl:SelectAllOnFocus(true)
+
+ local hookID = tostring({})
+ local textEntry = pnl
+ local delay = os.clock() + 0.3
+
+ local old_name = self:GetName()
+
+ pac.AddHook('Think', hookID, function(code)
+ if not IsValid(textEntry) then return pac.RemoveHook('Think', hookID) end
+ if textEntry:IsHovered() then return end
+ if delay > os.clock() then return end
+ if not input.IsMouseDown(MOUSE_LEFT) and not input.IsKeyDown(KEY_ESCAPE) then return end
+ pac.RemoveHook('Think', hookID)
+ textEntry:Remove()
+ pnl:OnEnter()
+ end)
+
+ --local x,y = pnl:GetPos()
+ --pnl:SetPos(x+3,y-4)
+ --pnl:Dock(FILL)
+ local x, y = self.pace_tree_node.Label:LocalToScreen()
+ local inset_x = self.pace_tree_node.Label:GetTextInset()
+ pnl:SetPos(x + inset_x, y)
+ pnl:SetSize(self.pace_tree_node.Label:GetSize())
+ pnl:SetWide(ScrW())
+ pnl:MakePopup()
+
+ pnl.OnEnter = function()
+ local input_text = pnl:GetText()
+ pnl:Remove()
+ if old_name == input_text then return end
+ self:SetName(input_text)
+ if self.pace_tree_node then
+ if input_text ~= "" then
+ self.pace_tree_node:SetText(input_text)
+ else
+ timer.Simple(0, function()
+ self.pace_tree_node:SetText(self:GetName())
+ end)
+ end
+ end
+ pace.PopulateProperties(self)
+ end
+
+ local old = pnl.Paint
+ pnl.Paint = function(...)
+ if not self:IsValid() then pnl:Remove() return end
+
+ surface.SetFont(pnl:GetFont())
+ local w = surface.GetTextSize(pnl:GetText()) + 6
+
+ surface.DrawRect(0, 0, w, pnl:GetTall())
+ surface.SetDrawColor(self.pace_tree_node:GetSkin().Colours.Properties.Border)
+ surface.DrawOutlinedRect(0, 0, w, pnl:GetTall())
+
+ pnl:SetWide(w)
+
+ old(...)
+ end
+ elseif pace.doubleclickfunc == "notes" then
+ if IsValid(pace.notes_pnl) then
+ pace.notes_pnl:Remove()
+ end
+ local pnl = vgui.Create("DFrame")
+ pace.notes_pnl = pnl
+ local DText = vgui.Create("DTextEntry", pnl)
+ local DButtonOK = vgui.Create("DButton", pnl)
+ DText:SetMaximumCharCount(50000)
+
+ pnl:SetSize(1200,800)
+ pnl:SetTitle("Long text with newline support for Notes.")
+ pnl:Center()
+ DButtonOK:SetText("OK")
+ DButtonOK:SetSize(80,20)
+ DButtonOK:SetPos(500, 775)
+ DText:SetPos(5,25)
+ DText:SetSize(1190,700)
+ DText:SetMultiline(true)
+ DText:SetContentAlignment(7)
+ pnl:MakePopup()
+ DText:RequestFocus()
+ DText:SetText(self:GetNotes())
+
+ DButtonOK.DoClick = function()
+ self:SetNotes(DText:GetText())
+ pace.RefreshTree(true)
+ pnl:Remove()
+ end
+ elseif pace.doubleclickfunc == "showhide" then
+ self:SetHide(not self:GetHide())
+ end
+ end
+
+ local pac_doubleclick_specified = CreateClientConVar("pac_doubleclick_action_specified", "2", true, true, "Whether the base_part functions for double-click should be replaced by specific functions when available.\n\nset to 0 : only use base_class actions (expand, rename, notes, showhide)\nset to 1 : Use specific actions. most single-shot parts will trigger (sounds play, commands run, hitscans fire etc.), and events will invert\nset to 2 : When appropriate, some event types will have even more specific actions. command events trigger or toggle (depending on the time), is_flashlight_on will toggle the flashlight, timerx events will reset\n\nIf your selected base action is none, These won't trigger.\n\nIf you only want specific actions, you may select specific_only in the pac_doubleclick_action command if you only want specifics")
+ function PART:OnDoubleClickSpecified()
+ --override
+ end
+
+ function PART:DoDoubleClick()
+ pace.doubleclickfunc = pac_doubleclick_type:GetString()
+ if pace.doubleclickfunc == "none" or pace.doubleclickfunc == "" then return end
+ if pac_doubleclick_specified:GetInt() ~= 0 and self.ImplementsDoubleClickSpecified then
+ pace.FlashNotification("double click action : class-specific")
+ self:OnDoubleClickSpecified()
+ else
+ self:OnDoubleClickBaseClass()
+ end
+ end
end
do -- hidden / events
@@ -695,9 +869,49 @@ do -- hidden / events
return "pac_hide_disturbing is set to 1"
end
+ for i,part in ipairs(self:GetParentList()) do
+ if part:IsHidden() then
+ table_insert(found, tostring(part) .. " is parent hiding")
+ end
+ end
+ if found[1] then
+ return table.concat(found, "\n")
+ end
+
return ""
end
+ function PART:GetReasonsHidden()
+ local found = {}
+
+ for part in pairs(self.active_events) do
+ found[part] = "event hiding"
+ end
+
+ if self.Hide then
+ found[self] = "self hiding"
+ end
+
+ if self.hide_disturbing then
+ if self.Hide then
+ found[self] = "self hiding and disturbing"
+ else
+ found[self] = "disturbing"
+ end
+ end
+
+ for i,part in ipairs(self:GetParentList()) do
+ if not found[part] then
+ if part:IsHidden() then
+ found[part] = "parent hidden"
+ end
+ end
+ end
+
+ return found
+ end
+
+ local extra_dynamic = CreateClientConVar("pac_special_property_update_dynamically", "1", true, false, "Whether proxies should refresh the properties, and some booleans may show more information.")
function PART:CalcShowHide(from_rendering)
local b = self:IsHidden()
@@ -710,6 +924,24 @@ do -- hidden / events
end
self.last_hidden = b
+ if pace.IsActive() then
+ if self == pace.current_part then --update the hide property (show reasons why it's hidden)
+ if IsValid(self.hide_property_pnl) then
+ local reasons_hidden = self:GetReasonsHidden()
+ local pnl = self.hide_property_pnl:GetParent()
+ if not table.IsEmpty(reasons_hidden) and not self.reasons_hidden then
+ self.reasons_hidden = reasons_hidden
+ pnl:SetTooltip("Hidden by:" .. table.ToString(reasons_hidden, "", true))
+ if not extra_dynamic:GetBool() then return end
+ pnl:CreateAlternateLabel("hidden")
+ else
+ pnl:CreateAlternateLabel(nil) --remove it
+ self.reasons_hidden = nil
+ pnl:SetTooltip()
+ end
+ end
+ end
+ end
end
function PART:IsHiddenCached()
@@ -1149,4 +1381,102 @@ do
function PART:AlwaysOnThink() end
end
+function PART:GetTutorial()
+ if pace and pace.TUTORIALS and pace.TUTORIALS[self.ClassName] then
+ return pace.TUTORIALS.PartInfos[self.ClassName].popup_tutorial or ""
+ end
+end
+
+--the popup system
+function PART:SetupEditorPopup(str, force_open, tbl, x, y)
+ if pace.Editor == nil then return end
+ if self.pace_tree_node == nil then return end
+ local legacy_help_popup_hack = false
+ if not tbl then
+ legacy_help_popup_hack = false
+ elseif tbl.from_legacy then
+ legacy_help_popup_hack = true
+ end
+ if not IsValid(self) then return end
+
+ local popup_config_table = tbl or {
+ pac_part = self, obj_type = GetConVar("pac_popups_preferred_location"):GetString(),
+ hoverfunc = function() end,
+ doclickfunc = function() end,
+ panel_exp_width = 900, panel_exp_height = 400
+ }
+
+ local default_state = str == nil or str == ""
+ local info_string
+ if self.ClassName == "event" and default_state then
+ info_string = self:GetEventTutorialText()
+ end
+ info_string = info_string or str or self.ClassName .. "\nno special information available"
+
+ if default_state and pace then
+ local partsize_tbl = pace.GetPartSizeInformation(self)
+ info_string = info_string .. "\n" .. partsize_tbl.info .. ", " .. partsize_tbl.all_share_percent .. "% of all parts"
+ end
+
+ if self.Notes and self.Notes ~= "" then
+ info_string = info_string .. "\n\nNotes:\n\n" .. self.Notes
+ end
+
+ local tree_node = self.pace_tree_node
+ local part = self
+ self.killpopup = false
+ local pnl
+
+ --local pace = pace or {}
+ if tree_node and tree_node.Label then
+ local part = self
+
+ function tree_node:Think()
+ --if not part.killpopup and ((self.Label:IsHovered() and GetConVar("pac_popups_preferred_location"):GetString() == "pac tree label") or input.IsButtonDown(KEY_F1) or force_open) then
+ if not part.killpopup and ((self.Label:IsHovered() and GetConVar("pac_popups_preferred_location"):GetString() == "pac tree label") or force_open) then
+ if not self.popuppnl_is_up and not IsValid(self.popupinfopnl) and not part.killpopup and not legacy_help_popup_hack then
+ self.popupinfopnl = pac.InfoPopup(
+ info_string,
+ popup_config_table
+ )
+ self.popuppnl_is_up = true
+ end
+
+ --if IsValid(self.popupinfopnl) then self.popupinfopnl:MakePopup() end
+ pnl = self.popupinfopnl
+
+ end
+ if not IsValid(self.popupinfopnl) then self.popupinfopnl = nil self.popuppnl_is_up = false end
+ end
+ tree_node:Think()
+ end
+ if not pnl then
+ pnl = pac.InfoPopup(info_string,popup_config_table, x, y)
+ if IsValid(self.pace_tree_node) then self.pace_tree_node.popupinfopnl = pnl end
+ end
+ if pace then
+ pace.legacy_floating_popup_reserved = pnl
+ end
+
+ return pnl
+end
+
+function PART:AttachEditorPopup(str, flash, tbl, x, y)
+ local pnl = self:SetupEditorPopup(str, flash, tbl, x, y)
+ if flash and pnl then
+ pnl:MakePopup()
+ end
+ return pnl
+end
+
+function PART:DetachEditorPopup()
+ local tree_node = self.pace_tree_node
+ if tree_node then
+ if tree_node.popupinfopnl then
+ tree_node.popupinfopnl:Remove()
+ end
+ if not IsValid(tree_node.popupinfopnl) then tree_node.popupinfopnl = nil end
+ end
+end
+
BUILDER:Register()
diff --git a/lua/pac3/core/client/bones.lua b/lua/pac3/core/client/bones.lua
index 477c506dd..71f3aab04 100644
--- a/lua/pac3/core/client/bones.lua
+++ b/lua/pac3/core/client/bones.lua
@@ -180,34 +180,35 @@ function pac.ResetBoneCache(ent)
end
end
-local UP = Vector(0,0,1):Angle()
+local viewmodelClassName = "viewmodel"
local function GetBonePosition(ent, id)
- local pos, ang = ent:GetBonePosition(id)
+ local mat = ent:GetBoneMatrix(id)
+ if not mat then return end
- if not pos then return end
+ local pos, ang = mat:GetTranslation(), mat:GetAngles()
if ang.p ~= ang.p then ang.p = 0 end
if ang.y ~= ang.y then ang.y = 0 end
if ang.r ~= ang.r then ang.r = 0 end
- if pos == ent:GetPos() then
- local mat = ent:GetBoneMatrix(id)
- if mat then
- pos = mat:GetTranslation()
- ang = mat:GetAngles()
- end
- end
+ if ent == pac.LocalHands or ent:GetClass() == viewmodelClassName then
+ local owner = ent:GetOwner()
+
+ if owner:IsPlayer() then
+ local ownerwep = owner:GetActiveWeapon()
- if (ent:GetClass() == "viewmodel" or ent == pac.LocalHands) and
- ent:GetOwner():IsPlayer() and ent:GetOwner():GetActiveWeapon().ViewModelFlip then
- ang.r = -ang.r
+ if ownerwep:IsValid() and ownerwep.ViewModelFlip then
+ ang.r = -ang.r
+ end
+ end
end
return pos, ang
end
local angle_origin = Angle(0,0,0)
+local UP = Vector(0,0,1):Angle()
function pac.GetBonePosAng(ent, id, parent)
if not ent:IsValid() then return Vector(), Angle() end
@@ -222,8 +223,15 @@ function pac.GetBonePosAng(ent, id, parent)
if enabled then
if target:IsValid() then
if bone ~= 0 then
- local wpos, wang = target:GetBonePosition(target:TranslatePhysBoneToBone(bone))
- endpos = LocalToWorld(hitpos, Angle(), wpos, wang)
+ local mat = target:GetBoneMatrix(target:TranslatePhysBoneToBone(bone))
+ local wpos, wang
+
+ if mat then
+ wpos, wang = mat:GetTranslation(), mat:GetAngles()
+ endpos = LocalToWorld(hitpos, Angle(), wpos, wang)
+ else
+ endpos = target:LocalToWorld(hitpos)
+ end
else
endpos = target:LocalToWorld(hitpos)
end
diff --git a/lua/pac3/core/client/ear_grab_animation.lua b/lua/pac3/core/client/ear_grab_animation.lua
index 95c651c62..511def904 100644
--- a/lua/pac3/core/client/ear_grab_animation.lua
+++ b/lua/pac3/core/client/ear_grab_animation.lua
@@ -1,5 +1,5 @@
-- see https://github.com/Facepunch/garrysmod/blob/master/garrysmod/gamemodes/base/gamemode/animations.lua#L235
-hook.Add("PostGamemodeLoaded", "pac_ear_grab_animation",function()
+pac.AddHook("PostGamemodeLoaded", "ear_grab_animation", function()
if GAMEMODE.GrabEarAnimation then -- only add it if it exists
local original_ear_grab_animation = GAMEMODE.GrabEarAnimation
GAMEMODE.GrabEarAnimation = function(_, ply)
diff --git a/lua/pac3/core/client/init.lua b/lua/pac3/core/client/init.lua
index c96b0d63f..92faf69bc 100644
--- a/lua/pac3/core/client/init.lua
+++ b/lua/pac3/core/client/init.lua
@@ -27,14 +27,15 @@ do
pac_enable:SetBool(true)
end
- function pac.Disable()
+ function pac.Disable(temp)
pac.EnableDrawnEntities(false)
pac.DisableAddedHooks()
pac.CallHook("Disable")
- pac_enable:SetBool(false)
+ if not temp then
+ pac_enable:SetBool(false)
+ end
end
end
-
include("util.lua")
include("class.lua")
@@ -58,7 +59,7 @@ include("ear_grab_animation.lua")
pac.LoadParts()
-hook.Add("OnEntityCreated", "pac_init", function(ent)
+pac.AddHook("OnEntityCreated", "init", function(ent)
local ply = LocalPlayer()
if not ply:IsValid() then return end
@@ -67,8 +68,8 @@ hook.Add("OnEntityCreated", "pac_init", function(ent)
pac.LocalHands = pac.LocalPlayer:GetHands()
pac.in_initialize = true
- hook.Run("pac_Initialized")
+ pac.CallHook("Initialized")
pac.in_initialize = nil
- hook.Remove("OnEntityCreated", "pac_init")
+ pac.RemoveHook("OnEntityCreated", "init")
end)
diff --git a/lua/pac3/core/client/integration_tools.lua b/lua/pac3/core/client/integration_tools.lua
index caa033c4e..380266e15 100644
--- a/lua/pac3/core/client/integration_tools.lua
+++ b/lua/pac3/core/client/integration_tools.lua
@@ -25,7 +25,7 @@ end
do
local force_draw_localplayer = false
- hook.Add("ShouldDrawLocalPlayer", "pac_draw_2d_entity", function()
+ pac.AddHook("ShouldDrawLocalPlayer", "draw_2d_entity", function()
if force_draw_localplayer == true then
return true
end
@@ -244,7 +244,7 @@ function pac.AddEntityClassListener(class, session, check_func, draw_dist)
draw_dist = 0
check_func = check_func or function(ent) return ent:GetClass() == class end
- local id = "pac_auto_attach_" .. class
+ local id = "auto_attach_" .. class
local weapons = {}
local function weapon_think()
@@ -282,7 +282,7 @@ function pac.AddEntityClassListener(class, session, check_func, draw_dist)
if ent:IsValid() and check_func(ent) then
if ent:IsWeapon() then
weapons[ent:EntIndex()] = ent
- hook.Add("Think", id, weapon_think)
+ pac.AddHook("Think", id, weapon_think)
else
pac.SetupENT(ent)
ent:AttachPACSession(session)
@@ -302,8 +302,8 @@ function pac.AddEntityClassListener(class, session, check_func, draw_dist)
created(ent)
end
- hook.Add("EntityRemoved", id, removed)
- hook.Add("OnEntityCreated", id, created)
+ pac.AddHook("EntityRemoved", id, removed)
+ pac.AddHook("OnEntityCreated", id, created)
end
function pac.RemoveEntityClassListener(class, session, check_func)
@@ -319,11 +319,11 @@ function pac.RemoveEntityClassListener(class, session, check_func)
end
end
- local id = "pac_auto_attach_" .. class
+ local id = "auto_attach_" .. class
- hook.Remove("Think", id)
- hook.Remove("EntityRemoved", id)
- hook.Remove("OnEntityCreated", id)
+ pac.RemoveHook("Think", id)
+ pac.RemoveHook("EntityRemoved", id)
+ pac.RemoveHook("OnEntityCreated", id)
end
timer.Simple(0, function()
diff --git a/lua/pac3/core/client/owner_name.lua b/lua/pac3/core/client/owner_name.lua
index c79f81e64..a405f5e21 100644
--- a/lua/pac3/core/client/owner_name.lua
+++ b/lua/pac3/core/client/owner_name.lua
@@ -43,10 +43,10 @@ function pac.GetWorldEntity()
if not pac.WorldEntity:IsValid() then
local ent = pac.CreateEntity("models/error.mdl")
- ent:SetPos(Vector(0,0,0))
+ ent:SetPos(Vector(0, 0, 0))
-- go away ugh
- ent:SetModelScale(0,0)
+ ent:SetMaterial("materials/null")
ent.IsPACWorldEntity = true
diff --git a/lua/pac3/core/client/part_pool.lua b/lua/pac3/core/client/part_pool.lua
index 8bf2da5a9..c2333916e 100644
--- a/lua/pac3/core/client/part_pool.lua
+++ b/lua/pac3/core/client/part_pool.lua
@@ -44,6 +44,7 @@ local ent_parts = _G.pac_local_parts or {}
local all_parts = _G.pac_all_parts or {}
local uid_parts = _G.pac_uid_parts or {}
+
if game.SinglePlayer() or (player.GetCount() == 1 and LocalPlayer():IsSuperAdmin()) then
_G.pac_local_parts = ent_parts
_G.pac_all_parts = all_parts
@@ -250,7 +251,7 @@ function pac.UnhookEntityRender(ent, part)
end
pac.AddHook("Think", "events", function()
- for _, ply in ipairs(player.GetAll()) do
+ for _, ply in player.Iterator() do
if not ent_parts[ply] then continue end
if pac.IsEntityIgnored(ply) then continue end
@@ -348,8 +349,8 @@ pac.AddHook("Think", "events", function()
lamp:SetAngles( pac.LocalPlayer:EyeAngles() )
lamp:Update()
- hook.Add("PostRender", "pac_flashlight_stuck_fix", function()
- hook.Remove("PostRender", "pac_flashlight_stuck_fix")
+ pac.AddHook("PostRender", "flashlight_stuck_fix", function()
+ pac.RemoveHook("PostRender", "flashlight_stuck_fix")
lamp:Remove()
end)
@@ -498,6 +499,33 @@ function pac.GetPartFromUniqueID(owner_id, id)
return uid_parts[owner_id] and uid_parts[owner_id][id] or NULL
end
+function pac.FindPartByPartialUniqueID(owner_id, crumb)
+ if not crumb then return NULL end
+ if not isstring(crumb) then return NULL end
+ if #crumb <= 3 then return NULL end
+ local closest_match
+ local length_of_closest_match = 0
+ if uid_parts[owner_id] then
+ if uid_parts[owner_id][crumb] then
+ return uid_parts[owner_id][crumb]
+ end
+
+ for _, part in pairs(uid_parts[owner_id]) do
+ local start_i,end_i = string.find(part.UniqueID, crumb)
+ if start_i or end_i then
+ closest_match = part
+ if length_of_closest_match < end_i - start_i + 1 then
+ closest_match = part
+ length_of_closest_match = end_i - start_i + 1
+ end
+
+ end
+ end
+
+ end
+ return closest_match or NULL
+end
+
function pac.FindPartByName(owner_id, str, exclude)
if uid_parts[owner_id] then
if uid_parts[owner_id][str] then
@@ -617,6 +645,79 @@ function pac.EnablePartsByClass(classname, enable)
end
end
+function pac.LinkSpecialTrackedPartsForEvent(part, ply)
+ part.erroring_cached_parts = {}
+ part.found_cached_parts = {}
+
+ part.specialtrackedparts = {}
+ local tracked_classes = {
+ ["damage_zone"] = true,
+ ["lock"] = true
+ }
+ for _,part2 in pairs(all_parts) do
+ if ply == part2:GetPlayerOwner() and tracked_classes[part2.ClassName] then
+ table.insert(part.specialtrackedparts,part2)
+ end
+ end
+end
+
+--a centralized function to cache a part in a prebuilt list so we can access relevant parts already narrowed down instead of searching through all parts / localparts
+function pac.RegisterPartToCache(ply, name, part, remove)
+ if not IsValid(ply) then return end
+ ply["pac_part_cache_"..name] = ply["pac_part_cache_"..name] or {}
+ if remove then
+ ply["pac_part_cache_"..name][part] = nil
+ else
+ ply["pac_part_cache_"..name][part] = part
+ end
+end
+
+function pac.UpdateButtonEvents(ply, key, down)
+ local button_events = ply.pac_part_cache_button_events or {}
+ for _,part in pairs(button_events) do
+ if part:GetProperty("ignore_if_hidden") then
+ if part:IsHidden() then continue end
+ end
+ if key ~= string.Split(part.Arguments, "@@")[1]:lower() then continue end
+ part.pac_broadcasted_buttons_holduntil = part.pac_broadcasted_buttons_holduntil or {}
+ part.toggleimpulsekey = part.toggleimpulsekey or {}
+ part.toggleimpulsekey[key] = down
+ part.pac_broadcasted_buttons_holduntil[key] = part.pac_broadcasted_buttons_holduntil[key] or 0
+ ply.pac_broadcasted_buttons_lastpressed[key] = ply.pac_broadcasted_buttons_lastpressed[key] or 0
+
+ if part.holdtime == nil then part.holdtime = 0 end
+ part.pac_broadcasted_buttons_holduntil[key] = ply.pac_broadcasted_buttons_lastpressed[key] + part.holdtime
+
+ if part.togglestate == nil then part.togglestate = false end
+
+ if part.toggleimpulsekey[key] then
+ part.togglestate = not part.togglestate
+ end
+ end
+end
+
+function pac.StopSound()
+ for _,part in pairs(all_parts) do
+ if part.ClassName == "sound" or part.ClassName == "sound2" or part.ClassName == "ogg" or part.ClassName == "webaudio" then
+ part:StopSound(true)
+ end
+ end
+end
+
+function pac.ForceUpdateSoundVolumes()
+ for _,part in pairs(all_parts) do
+ if part.ClassName == "sound" then
+ if part.csptch then part.csptch:ChangeVolume(math.Clamp(part.Volume * pac.volume, 0.001, 1), 0) end
+ elseif part.ClassName == "sound2" or part.ClassName == "ogg" then
+ if part.last_stream and part.last_stream.SetVolume then part.last_stream:SetVolume(part.Volume * pac.volume) end
+ elseif part.ClassName == "webaudio" then
+ for key, stream in pairs(part.streams) do
+ if stream and stream.SetVolume then stream:SetVolume(part.Volume * pac.volume) end
+ end
+ end
+ end
+end
+
cvars.AddChangeCallback("pac_hide_disturbing", function()
for _, part in pairs(all_parts) do
if part:IsValid() then
diff --git a/lua/pac3/core/client/parts/beam.lua b/lua/pac3/core/client/parts/beam.lua
index ab83b816e..85919fcf6 100644
--- a/lua/pac3/core/client/parts/beam.lua
+++ b/lua/pac3/core/client/parts/beam.lua
@@ -24,7 +24,7 @@ do
local vector = Vector()
local color = Color(255, 255, 255, 255)
- function pac.DrawBeam(veca, vecb, dira, dirb, bend, res, width, start_color, end_color, frequency, tex_stretch, tex_scroll, width_bend, width_bend_size, width_start_mul, width_end_mul)
+ function pac.DrawBeam(veca, vecb, dira, dirb, bend, res, width, start_color, end_color, frequency, tex_stretch, tex_scroll, width_bend, width_bend_size, width_start_mul, width_end_mul, width_pow)
if not veca or not vecb or not dira or not dirb then return end
@@ -46,6 +46,7 @@ do
tex_scroll = tex_scroll or 0
width_start_mul = width_start_mul or 1
width_end_mul = width_end_mul or 1
+ width_pow = width_pow or 1
render_StartBeam(res + 1)
@@ -66,7 +67,7 @@ do
render_AddBeam(
vector,
- (width + ((math_sin(wave) ^ width_bend_size) * width_bend)) * Lerp(frac, width_start_mul, width_end_mul),
+ (width + ((math_sin(wave) ^ width_bend_size) * width_bend)) * Lerp(math.pow(frac,width_pow), width_start_mul, width_end_mul),
(i / tex_stretch) + tex_scroll,
color
)
@@ -90,16 +91,28 @@ BUILDER:StartStorableVars()
BUILDER:PropertyOrder("ParentName")
BUILDER:GetSet("Material", "cable/rope")
BUILDER:GetSetPart("EndPoint")
- BUILDER:GetSet("Bend", 10)
- BUILDER:GetSet("Frequency", 1)
- BUILDER:GetSet("Resolution", 16)
+ BUILDER:GetSet("MultipleEndPoints","")
+ BUILDER:GetSet("AutoHitpos", false, {description = "Create the endpoint at the hit position in front of the part (red arrow)"})
+ BUILDER:GetSet("AutoHitposFilter", "standard", {enums = {
+ standard = "standard",
+ world_only = "world_only",
+ life = "life",
+ none = "none"
+ }, description = "the filter modes are as such: standard = exclude player, root owner and pac_projectile\nworld_only = only hit world\nlife = hit players, NPCs, Nextbots\nnone = hit anything"})
+ BUILDER:SetPropertyGroup("beam size")
BUILDER:GetSet("Width", 1)
BUILDER:GetSet("WidthBend", 0)
BUILDER:GetSet("WidthBendSize", 1)
BUILDER:GetSet("StartWidthMultiplier", 1)
BUILDER:GetSet("EndWidthMultiplier", 1)
+ BUILDER:GetSet("WidthMorphPower", 1)
+ BUILDER:SetPropertyGroup("beam detail")
+ BUILDER:GetSet("Bend", 10)
+ BUILDER:GetSet("Frequency", 1)
+ BUILDER:GetSet("Resolution", 16)
BUILDER:GetSet("TextureStretch", 1)
BUILDER:GetSet("TextureScroll", 0)
+ BUILDER:GetSet("ScrollRate", 0)
BUILDER:SetPropertyGroup("orientation")
BUILDER:SetPropertyGroup("appearance")
BUILDER:GetSet("StartColor", Vector(255, 255, 255), {editor_panel = "color"})
@@ -108,6 +121,19 @@ BUILDER:StartStorableVars()
BUILDER:GetSet("EndAlpha", 1)
BUILDER:SetPropertyGroup("other")
BUILDER:PropertyOrder("DrawOrder")
+ BUILDER:SetPropertyGroup("Showtime dynamics")
+ BUILDER:GetSet("EnableDynamics", false, {description = "If you want to make a fading effect, you can do it here instead of adding proxies."})
+ BUILDER:GetSet("SizeFadeSpeed", 1)
+ BUILDER:GetSet("SizeFadePower", 1)
+ BUILDER:GetSet("IncludeWidthBend", true, {description = "whether to include the width bend in the dynamics fading of the overall width multiplier"})
+ BUILDER:GetSet("DynamicsStartSizeMultiplier", 1, {editor_friendly = "StartSizeMultiplier"})
+ BUILDER:GetSet("DynamicsEndSizeMultiplier", 1, {editor_friendly = "EndSizeMultiplier"})
+
+ BUILDER:GetSet("AlphaFadeSpeed", 1)
+ BUILDER:GetSet("AlphaFadePower", 1)
+ BUILDER:GetSet("DynamicsStartAlpha", 1, {editor_sensitivity = 0.25, editor_clamp = {0, 1}, editor_friendly = "StartAlpha"})
+ BUILDER:GetSet("DynamicsEndAlpha", 1, {editor_sensitivity = 0.25, editor_clamp = {0, 1}, editor_friendly = "EndAlpha"})
+
BUILDER:EndStorableVars()
function PART:GetNiceName()
@@ -122,6 +148,68 @@ function PART:Initialize()
self.EndColorC = Color(255, 255, 255, 255)
end
+function PART:GetOrFindCachedPart(uid_or_name)
+ local part = nil
+ self.erroring_cached_parts = {}
+ self.found_cached_parts = self.found_cached_parts or {}
+ if self.found_cached_parts[uid_or_name] then self.erroring_cached_parts[uid_or_name] = nil return self.found_cached_parts[uid_or_name] end
+ if self.erroring_cached_parts[uid_or_name] then return end
+
+ local owner = self:GetPlayerOwner()
+ part = pac.GetPartFromUniqueID(pac.Hash(owner), uid_or_name) or pac.FindPartByPartialUniqueID(pac.Hash(owner), uid_or_name)
+ if not part:IsValid() then
+ part = pac.FindPartByName(pac.Hash(owner), uid_or_name, self)
+ else
+ self.found_cached_parts[uid_or_name] = part
+ return part
+ end
+ if not part:IsValid() then
+ self.erroring_cached_parts[uid_or_name] = true
+ else
+ self.found_cached_parts[uid_or_name] = part
+ return part
+ end
+ return part
+end
+
+function PART:SetMultipleEndPoints(str)
+ self.MultipleEndPoints = str
+ if str == "" then self.MultiEndPoint = nil self.ExtraHermites = nil return end
+ timer.Simple(0.2, function()
+ if not string.find(str, ";") then
+ local part = self:GetOrFindCachedPart(str)
+ if IsValid(part) then
+ self:SetEndPoint(part)
+ self.MultipleEndPoints = ""
+ else
+ timer.Simple(3, function()
+ local part = self:GetOrFindCachedPart(str)
+ if part then
+ self:SetEndPoint(part)
+ self.MultipleEndPoints = ""
+ end
+ end)
+ end
+ self.MultiEndPoint = nil
+ else
+ self:SetEndPoint()
+ self.MultiEndPoint = {}
+ self.ExtraHermites = {}
+ local uid_splits = string.Split(str, ";")
+ for i,uid2 in ipairs(uid_splits) do
+ local part = self:GetOrFindCachedPart(uid2)
+ if not IsValid(part) then
+ timer.Simple(3, function()
+ local part = self:GetOrFindCachedPart(uid2)
+ if part then table.insert(self.MultiEndPoint, part) table.insert(self.ExtraHermites, part) end
+ end)
+ else table.insert(self.MultiEndPoint, part) table.insert(self.ExtraHermites, part) end
+ end
+ self.ExtraHermites_Property = "MultipleEndPoints"
+ end
+ end)
+end
+
function PART:SetStartColor(v)
self.StartColorC = self.StartColorC or Color(255, 255, 255, 255)
@@ -198,33 +286,109 @@ function PART:SetMaterial(var)
end
end
+function PART:OnShow()
+ self.starttime = CurTime()
+ self.scrolled_amount = 0
+end
+
function PART:OnDraw()
local part = self.EndPoint
- if self.Materialm and self.StartColorC and self.EndColorC and part:IsValid() and part.GetWorldPosition then
+ local lifetime = (CurTime() - self.starttime)
+ self.scrolled_amount = self.scrolled_amount + FrameTime() * self.ScrollRate
+
+ local fade_factor_w = math.Clamp(lifetime*self.SizeFadeSpeed,0,1)
+ local fade_factor_a = math.Clamp(lifetime*self.AlphaFadeSpeed,0,1)
+
+ local final_alpha_mult = self.EnableDynamics and
+ self.DynamicsStartAlpha + (self.DynamicsEndAlpha - self.DynamicsStartAlpha) * math.pow(fade_factor_a,self.AlphaFadePower)
+ or 1
+
+ local StartColorA = self.StartColorC.a
+ local EndColorA = self.EndColorC.a
+ self.StartColorC.a = final_alpha_mult * StartColorA
+ self.EndColorC.a = final_alpha_mult * EndColorA
+
+ local final_size_mult = self.EnableDynamics and
+ self.DynamicsStartSizeMultiplier + (self.DynamicsEndSizeMultiplier - self.DynamicsStartSizeMultiplier) * math.pow(fade_factor_w,self.SizeFadePower)
+ or 1
+
+ if self.Materialm and self.StartColorC and self.EndColorC and ((part:IsValid() and part.GetWorldPosition) or self.MultiEndPoint or self.AutoHitpos) then
local pos, ang = self:GetDrawPosition()
render.SetMaterial(self.Materialm)
- pac.DrawBeam(
- pos,
- part:GetWorldPosition(),
-
- ang:Forward(),
- part:GetWorldAngles():Forward(),
-
- self.Bend,
- math.Clamp(self.Resolution, 1, 256),
- self.Width,
- self.StartColorC,
- self.EndColorC,
- self.Frequency,
- self.TextureStretch,
- self.TextureScroll,
- self.WidthBend,
- self.WidthBendSize,
- self.StartWidthMultiplier,
- self.EndWidthMultiplier
- )
+ if self.MultiEndPoint then
+ for _,part in ipairs(self.MultiEndPoint) do
+ pac.DrawBeam(
+ pos,
+ part:GetWorldPosition(),
+
+ ang:Forward(),
+ part:GetWorldAngles():Forward(),
+
+ self.Bend,
+ math.Clamp(self.Resolution, 1, 256),
+ self.Width * final_size_mult,
+ self.StartColorC,
+ self.EndColorC,
+ self.Frequency,
+ self.TextureStretch,
+ self.TextureScroll - self.scrolled_amount,
+ self.IncludeWidthBend and final_size_mult * self.WidthBend or self.WidthBend,
+ self.WidthBendSize,
+ self.StartWidthMultiplier,
+ self.EndWidthMultiplier,
+ self.WidthMorphPower
+ )
+ end
+ else
+ if self.AutoHitpos then
+ local filter = {}
+ local playerowner = self:GetPlayerOwner()
+ local rootowner = self:GetRootPart():GetOwner()
+ if self.AutoHitposFilter == "standard" then
+ filter = function(ent)
+ if ent == playerowner then return false end
+ if ent == rootowner then return false end
+ if ent:GetClass() == "pac_projectile" then return false end
+ return true
+ end
+ elseif self.AutoHitposFilter == "world_only" then
+ filter = function(ent)
+ return ent:IsWorld()
+ end
+ elseif self.AutoHitposFilter == "life" then
+ filter = function(ent) return (ent:IsNPC() or (ent:IsPlayer() and ent ~= playerowner) or ent:IsNextBot()) end
+ else
+ filter = nil
+ end
+ self.hitpos = util.QuickTrace(pos, ang:Forward()*32000, filter).HitPos
+ end
+ pac.DrawBeam(
+ pos,
+ self.AutoHitpos and self.hitpos or part:GetWorldPosition(),
+
+ ang:Forward(),
+ self.AutoHitpos and ang:Forward() or part:GetWorldAngles():Forward(),
+
+ self.Bend,
+ math.Clamp(self.Resolution, 1, 256),
+ self.Width * final_size_mult,
+ self.StartColorC,
+ self.EndColorC,
+ self.Frequency,
+ self.TextureStretch,
+ self.TextureScroll - self.scrolled_amount,
+ self.IncludeWidthBend and final_size_mult * self.WidthBend or self.WidthBend,
+ self.WidthBendSize,
+ self.StartWidthMultiplier,
+ self.EndWidthMultiplier,
+ self.WidthMorphPower
+ )
+ end
end
+
+ self.StartColorC.a = StartColorA
+ self.EndColorC.a = EndColorA
end
BUILDER:Register()
diff --git a/lua/pac3/core/client/parts/camera.lua b/lua/pac3/core/client/parts/camera.lua
index 01319bf1b..828c95690 100644
--- a/lua/pac3/core/client/parts/camera.lua
+++ b/lua/pac3/core/client/parts/camera.lua
@@ -19,14 +19,206 @@ for i, ply in ipairs(player.GetAll()) do
ply.pac_cameras = nil
end
+pac.client_camera_parts = {}
+
+local function CheckCamerasAgain(ply)
+ local cams = ply.pac_cameras or {}
+ local fpos, fang, ffov, fnearz, ffarz
+
+ for _, part in pairs(cams) do
+ if (not part.inactive or part.priority) and not part:IsHidden() then
+ return true
+ end
+ end
+end
+
+function pac.RebuildCameras(restricted_search)
+ local found_cams = false
+ pac.LocalPlayer.pac_cameras = {}
+ pac.client_camera_parts = {}
+ local parts_to_check
+ if restricted_search then parts_to_check = pac.client_camera_parts else parts_to_check = pac.GetLocalParts() end
+ if table.IsEmpty(pac.client_camera_parts) then
+ parts_to_check = pac.GetLocalParts()
+ end
+ for _,part in pairs(parts_to_check) do
+ if part:IsValid() then
+ part.inactive = nil
+ if part.ClassName == "camera" then
+ pac.nocams = false
+ found_cams = true
+ pac.client_camera_parts[part.UniqueID] = part
+ if not part.inactive or not part:IsHidden() or part.priority then
+ pac.LocalPlayer.pac_cameras[part] = part
+ end
+ end
+
+ end
+ end
+ if not found_cams then
+ pac.nocams = true
+ end
+end
+
+function PART:CameraTakePriority(then_view)
+ self:GetPlayerOwner().pac_cameras = self:GetPlayerOwner().pac_cameras or {}
+ for _, part in pairs(self:GetPlayerOwner().pac_cameras) do
+ if part ~= self then
+ part.priority = false
+ part.inactive = true
+ part:RemoveSmallIcon()
+ end
+ end
+ self.priority = true
+ self.inactive = false
+ timer.Simple(0.02, function()
+ self.priority = true
+ end)
+ if then_view then
+ timer.Simple(0.2, function() pace.CameraPartSwapView(true) end)
+ end
+end
+
function PART:OnShow()
- local owner = self:GetRootPart():GetOwner()
+ local owner = self:GetPlayerOwner()
if not owner:IsValid() then return end
+ self.inactive = false
owner.pac_cameras = owner.pac_cameras or {}
owner.pac_cameras[self] = self
+
+ --the policy is that a shown camera takes priority over all others
+ self:CameraTakePriority()
+end
+
+function PART:OnHide()
+ local owner = self:GetPlayerOwner()
+ if not owner:IsValid() then return end
+
+ owner.pac_cameras = owner.pac_cameras or {}
+
+ --this camera cedes priority to others that may be active
+ for _, part in pairs(owner.pac_cameras) do
+ if part ~= self and not part:IsHidden() then
+ part.priority = true
+ end
+ end
+ self.inactive = true
+ self.priority = false
+ owner.pac_cameras[self] = nil
+ pac.TryToAwakenDormantCameras()
+end
+
+function PART:OnRemove()
+ local owner = self:GetPlayerOwner()
+
+ if LocalPlayer() == owner then
+ owner.pac_cameras = owner.pac_cameras or {}
+ pac.client_camera_parts[self.UniqueID] = nil
+ local other_visible_cameras = 0
+ --this camera cedes priority to others that may be active
+ for _, part in pairs(owner.pac_cameras) do
+ if part.UniqueID ~= self.UniqueID and not part:IsHidden() then
+ part.priority = true
+ other_visible_cameras = other_visible_cameras + 1
+ end
+ end
+ owner.pac_cameras[self] = nil
+ if not pace.hack_camera_part_donot_treat_wear_as_creating_part and not pace.is_still_loading_wearing then
+ timer.Simple(0.2, function()
+ pace.EnableView(true)
+ end)
+ timer.Simple(0.4, function()
+ pace.ResetView()
+ pace.CameraPartSwapView(true)
+ end)
+ end
+ if pac.active_camera == self then pac.active_camera = nil end
+ if pac.active_camera_manual == self then pac.active_camera_manual = nil end
+ pac.RebuildCameras()
+ end
+end
+
+local doing_calcshowhide = false
+
+function PART:PostOnCalcShowHide(hide)
+ if doing_calcshowhide then return end
+ doing_calcshowhide = true
+ timer.Simple(0.3, function()
+ doing_calcshowhide = false
+ end)
+ if hide then
+ if pac.active_camera_manual == self then --we're force-viewing this camera on the editor, assume we want to swap
+ pace.ManuallySelectCamera(self, false)
+ elseif not pac.awakening_dormant_cameras then
+ pac.TryToAwakenDormantCameras(self)
+ end
+ self:SetSmallIcon("event")
+ else
+ if pac.active_camera_manual then --we're force-viewing another camera on the editor, since we're showing a new camera, assume we want to swap
+ pace.ManuallySelectCamera(self, true)
+ end
+ self:SetSmallIcon("event")
+ end
+end
+
+--these hacks are outsourced instead of being on base part
+function PART:SetEventTrigger(event_part, enable)
+ if enable then
+ if not self.active_events[event_part] then
+ self.active_events[event_part] = event_part
+ self.active_events_ref_count = self.active_events_ref_count + 1
+ self:CallRecursive("CalcShowHide", false)
+ end
+
+ else
+ if self.active_events[event_part] then
+ self.active_events[event_part] = nil
+ self.active_events_ref_count = self.active_events_ref_count - 1
+ self:CallRecursive("CalcShowHide", false)
+ end
+ end
+
+ if pac.LocalPlayer == self:GetPlayerOwner() then
+ if event_part.Event == "command" then
+ pac.camera_linked_command_events[string.Split(event_part.Arguments,"@@")[1]] = true
+ end
+
+ self:PostOnCalcShowHide(enable)
+ end
+end
+
+function PART:CalcShowHide(from_rendering)
+ local b = self:IsHidden()
+
+ if b ~= self.last_hidden then
+ if b then
+ self:OnHide(from_rendering)
+ else
+ self:OnShow(from_rendering)
+ end
+ if pac.LocalPlayer == self:GetPlayerOwner() then
+ self:PostOnCalcShowHide(b)
+ end
+ end
+
+ self.last_hidden = b
+end
+
+
+
+function PART:Initialize()
+ if pac.LocalPlayer == self:GetPlayerOwner() then
+ pac.nocams = false
+ pac.client_camera_parts[self.UniqueID] = self
+ end
end
+--[[function PART:OnHide()
+ --only stop the part if explicitly set to hidden.
+ if not self.Hide and not self:IsHidden() then return end
+end]]
+
function PART:CalcView(_, _, eyeang, fov, nearz, farz)
local pos, ang = self:GetDrawPosition(nil, true)
@@ -49,28 +241,121 @@ end
BUILDER:Register()
+
local temp = {}
+local remaining_camera = false
+local remaining_camera_time_buffer = CurTime()
-pac.AddHook("CalcView", "camera_part", function(ply, pos, ang, fov, nearz, farz)
- if not ply.pac_cameras then return end
- if ply:GetViewEntity() ~= ply then return end
- for _, part in pairs(ply.pac_cameras) do
+function pac.TryToAwakenDormantCameras(calling_part)
+ if pace.still_loading_wearing then return end
+ if pace.Editor:IsValid() then return end
+ if pac.awakening_dormant_cameras then return end
+
+ if not isbool(calling_part) then
+ pac.RebuildCameras()
+ end
+ pac.awakening_dormant_cameras = true
+ for _,part in pairs(pac.client_camera_parts) do
if part:IsValid() then
- part:CalcShowHide()
-
- if not part:IsHidden() then
- pos, ang, fov, nearz, farz = part:CalcView(ply, pos, ang, fov, nearz, farz)
- temp.origin = pos
- temp.angles = ang
- temp.fov = fov
- temp.znear = nearz
- temp.zfar = farz
- temp.drawviewer = not part.DrawViewModel
- return temp
+ if part.ClassName == "camera" and part ~= calling_part then
+ part:GetRootPart():CallRecursive("Think")
+ end
+ end
+ end
+ timer.Simple(1, function()
+ pac.awakening_dormant_cameras = nil
+ end)
+
+ pace.EnableView(false)
+end
+
+pac.nocams = true
+pac.nocam_counter = 0
+
+function pac.HandleCameraPart(ply, pos, ang, fov, nearz, farz)
+ local chosen_part
+ local fpos, fang, ffov, fnearz, ffarz
+ local ply = pac.LocalPlayer
+ if pac.nocams then return end
+ ply.pac_cameras = ply.pac_cameras or {}
+
+ local warning_state = ply.pac_cameras == nil
+ if not warning_state then warning_state = table.IsEmpty(ply.pac_cameras) end
+ if ply:GetViewEntity() ~= ply then return end
+
+ remaining_camera = false
+ remaining_camera_time_buffer = remaining_camera_time_buffer or CurTime()
+ pace.delaymovement = RealTime() + 1 --we need to do that so that while testing cameras, you don't fly off when walking and end up far from your character
+ if warning_state then
+ pac.RebuildCameras(true)
+ pac.nocam_counter = pac.nocam_counter + 1
+ --go back to early returns to avoid looping through localparts when no cameras are active checked 500 times
+ if pac.nocam_counter > 500 then pac.nocams = true return end
+ else
+ if not IsValid(pac.active_camera) then pac.active_camera = nil pac.RebuildCameras(true) end
+ pac.nocam_counter = 0
+ local chosen_camera
+ for _, part in pairs(ply.pac_cameras) do
+ if part.ClassName ~= "camera" then
+ ply.pac_cameras[part] = nil
+ end
+ if part.ClassName == "camera" and part:IsValid() then
+ if not part:IsHidden() then
+ remaining_camera = true
+ remaining_camera_time_buffer = CurTime() + 0.1
+ end
+
+ part:CalcShowHide()
+ if not part.inactive then
+ --calculate values ahead of the return, used as a fallback just in case
+ fpos, fang, ffov, fnearz, ffarz = part:CalcView(_,_,ply:EyeAngles())
+ temp.origin = fpos
+ temp.angles = fang
+ temp.fov = ffov
+ temp.znear = fnearz
+ temp.zfar = ffarz
+ temp.drawviewer = false
+
+ if not part:IsHidden() and not part.inactive and part.priority then
+ pac.active_camera = part
+ temp.drawviewer = not part.DrawViewModel
+ chosen_camera = part
+ break
+ end
+ end
+ else
+ ply.pac_cameras[part] = nil
end
- else
- ply.pac_cameras[part] = nil
end
+
+
+ if chosen_camera then
+ chosen_camera:SetSmallIcon("icon16/eye.png")
+ return temp
+ end
+ end
+
+ if not pac.active_camera then
+ pac.RebuildCameras()
+ end
+ if remaining_camera or CurTime() < remaining_camera_time_buffer then
+ return temp
+ end
+
+ --final fallback, just give us any valid pac camera to preserve the view! priority will be handled elsewhere
+ if CheckCamerasAgain(ply) then
+ return temp
end
+ --only time to return to first person is if all camera parts are hidden AFTER we pass the buffer time filter
+ --until we make reversible first person a thing, letting some non-drawable parts think, this is the best solution I could come up with
+end
+
+function pac.HasRemainingCameraPart()
+ pac.RebuildCameras()
+ return table.Count(pac.LocalPlayer.pac_cameras) ~= 0
+end
+
+pac.AddHook("CalcView", "camera_part", function(ply, pos, ang, fov, nearz, farz)
+ pac.HandleCameraPart(ply, pos, ang, fov, nearz, farz)
end)
diff --git a/lua/pac3/core/client/parts/command.lua b/lua/pac3/core/client/parts/command.lua
index f902b6475..041af61fc 100644
--- a/lua/pac3/core/client/parts/command.lua
+++ b/lua/pac3/core/client/parts/command.lua
@@ -5,11 +5,46 @@ PART.ClassName = "command"
PART.Group = "advanced"
PART.Icon = "icon16/application_xp_terminal.png"
+PART.ImplementsDoubleClickSpecified = true
+
BUILDER:StartStorableVars()
- BUILDER:GetSet("String", "", {editor_panel = "string"})
+
+BUILDER:SetPropertyGroup("generic")
+ BUILDER:GetSet("String", "", {editor_panel = "code_script"})
BUILDER:GetSet("UseLua", false)
BUILDER:GetSet("ExecuteOnWear", false)
BUILDER:GetSet("ExecuteOnShow", true)
+ BUILDER:GetSet("SafeGuard", false, {description = "Delays the execution by 1 frame to attempt to prevent false triggers due to events' runtime quirks"})
+
+ --fading re-run mode
+ BUILDER:SetPropertyGroup("dynamic mode")
+ BUILDER:GetSet("DynamicMode", false, {description = "Dynamically assign an argument, adding the appended number to the string.\nWhen the appended number is changed, run the command again.\nFor example, it could be used with post processing fades. With pp_colormod 1, pp_colormod_color represents saturation multiplier. You could fade that to slowly fade to gray."})
+ BUILDER:GetSet("AppendedNumber", 1, {description = "Argument to use. When it changes, the command will run again with the updated value."})
+
+ --common alternate activations
+ BUILDER:SetPropertyGroup("alternates")
+ BUILDER:GetSet("OnHideString", "", {description = "An alternate command when the part is hidden. Governed by execute on show", editor_panel = "code_script"})
+ BUILDER:GetSet("DelayedString", "", {description = "An alternate command after a delay. Governed by execute on show", editor_panel = "code_script"})
+ BUILDER:GetSet("Delay", 1)
+
+ --we might as well have a section for command events since they are so useful for logic, and often integrated with command parts
+ --There should be a more convenient front-end for pac_event stuff and to fix the issue where people want to randomize their command (e.g. random attacks) when cs lua isn't allowed on some servers.
+ BUILDER:SetPropertyGroup("pac_event")
+ BUILDER:GetSet("CommandName", "", {description = "name of the pac_event to manage, or base name of the sequenced series.\n\nfor example, if you have commands hat1, hat2, hat3, and hat4:\n-the base name is hat\n-the minimum is 1\n-the maximum is 4"})
+ BUILDER:GetSet("Action", "Default", {enums = {
+ ["Default: single-shot"] = "Default",
+ ["Default: On (1)"] = "On",
+ ["Default: Off (0)"] = "Off",
+ ["Default: Toggle (2)"] = "Toggle",
+ ["Sequence: forward"] = "Sequence+",
+ ["Sequence: back"] = "Sequence-",
+ ["Sequence: set"] = "SequenceSet",
+ ["Random"] = "Random",
+ ["Random (Sequence set)"] = "RandomSet",
+ }, description = "The Default series corresponds to the normal pac_event command modes. Minimum and maximum don't apply.\nSequences run the sequence command pac_event_sequenced with the corresponding mode.\nRandom will select a random number to append to the base name and run the pac_event as a single-shot. This is intended to replace the lua randomizer method when sv_allowcslua is disabled."})
+ BUILDER:GetSet("Minimum", 1, {description = "The defined minimum for the pac_event if it's for a numbered series.\nOr, when using the sequence set action, this will be used.", editor_onchange = function(self, val) return math.floor(val) end})
+ BUILDER:GetSet("Maximum", 1, {description = "The defined maximum for the pac_event if it's for a numbered series.", editor_onchange = function(self, val) return math.floor(val) end})
+
BUILDER:EndStorableVars()
local sv_allowcslua = GetConVar("sv_allowcslua")
@@ -23,9 +58,54 @@ function PART:OnWorn()
end
end
+function PART:SetMaximum(val)
+ self.Maximum = val
+ if self:GetPlayerOwner() == pac.LocalPlayer and self.CommandName ~= "" and self.Minimum ~= self.Maximum then
+ self:GetPlayerOwner():ConCommand("pac_event_sequenced_force_set_bounds " .. self.CommandName .. " " .. self.Minimum .. " " .. self.Maximum)
+ end
+end
+
+function PART:SetMinimum(val)
+ self.Minimum = val
+ if self:GetPlayerOwner() == pac.LocalPlayer and self.CommandName ~= "" and self.Minimum ~= self.Maximum then
+ self:GetPlayerOwner():ConCommand("pac_event_sequenced_force_set_bounds " .. self.CommandName .. " " .. self.Minimum .. " " .. self.Maximum)
+ end
+end
+
+function PART:SetAppendedNumber(val)
+ if self.AppendedNumber ~= val then
+ self.AppendedNumber = val
+ if self:GetPlayerOwner() == pac.LocalPlayer and self.DynamicMode then
+ self:Execute()
+ end
+ end
+ self.AppendedNumber = val
+end
+
function PART:OnShow(from_rendering)
if not from_rendering and self:GetExecuteOnShow() then
- self:Execute()
+ if pace.still_loading_wearing then return end
+
+ if self.SafeGuard then
+ timer.Simple(0,function()
+ if self.Hide or self:IsHidden() then return end
+ self:Execute()
+ end)
+ else
+ self:Execute()
+ end
+
+ if self.DelayedString ~= "" then
+ timer.Simple(self.Delay, function()
+ self:Execute(self.DelayedString)
+ end)
+ end
+ end
+end
+
+function PART:OnHide()
+ if self.ExecuteOnShow and self.OnHideString ~= "" then
+ self:Execute(self.OnHideString)
end
end
@@ -34,12 +114,79 @@ function PART:SetUseLua(b)
self:SetString(self:GetString())
end
-function PART:SetString(str)
- if self.UseLua and canRunLua() and self:GetPlayerOwner() == pac.LocalPlayer then
- self.func = CompileString(str, "pac_event")
+function PART:HandleErrors(result, mode)
+ if isstring(result) then
+ pac.Message(result)
+ self.Error = "[" .. mode .. "] " .. result
+ self.erroring_mode = mode
+ self:SetError(result)
+ if pace.ActiveSpecialPanel and pace.ActiveSpecialPanel.luapad then
+ pace.ActiveSpecialPanel.special_title = self.Error
+ end
+ elseif isfunction(result) then
+ if pace.ActiveSpecialPanel and pace.ActiveSpecialPanel.luapad then
+ if not self.Error then --good compile
+ pace.ActiveSpecialPanel.special_title = "[" .. mode .. "] " .. "successfully compiled"
+ self.Error = nil
+ self:SetError()
+ elseif (self.erroring_mode~= nil and self.erroring_mode ~= mode) then --good compile but already had an error from somewhere else (there are 3 script areas: main, onhide, delayed)
+ pace.ActiveSpecialPanel.special_title = "successfully compiled, but another erroring script may remain at " .. self.erroring_mode
+ else -- if we fixed our previous error from the same mode
+ pace.ActiveSpecialPanel.special_title = "[" .. mode .. "] " .. "successfully compiled"
+ self.Error = nil
+ self:SetError()
+ end
+ end
end
+end
+function PART:SetString(str)
+ str = string.Trim(str,"\n")
+ self.func = nil
+ if self.UseLua and canRunLua() and self:GetPlayerOwner() == pac.LocalPlayer and str ~= "" then
+ self.func = CompileString(str, "pac_event", false)
+ self:HandleErrors(self.func, "Main string")
+ end
self.String = str
+ if self.UseLua and not canRunLua() then
+ self:SetError("clientside lua is disabled (sv_allowcslua 0)")
+ end
+end
+
+function PART:SetOnHideString(str)
+ str = string.Trim(str,"\n")
+ self.onhide_func = nil
+ if self.erroring_mode == "OnHide string" then self.erroring_mode = nil end
+ if self.UseLua and canRunLua() and self:GetPlayerOwner() == pac.LocalPlayer and str ~= "" then
+ self.onhide_func = CompileString(str, "pac_event", false)
+ self:HandleErrors(self.onhide_func, "OnHide string")
+ end
+ self.OnHideString = str
+ if self.UseLua and not canRunLua() then
+ self:SetError("clientside lua is disabled (sv_allowcslua 0)")
+ end
+end
+
+function PART:SetDelayedString(str)
+ str = string.Trim(str,"\n")
+ self.delayed_func = nil
+ if self.erroring_mode == "Delayed string" then self.erroring_mode = nil end
+ if self.UseLua and canRunLua() and self:GetPlayerOwner() == pac.LocalPlayer and str ~= "" then
+ self.delayed_func = CompileString(str, "pac_event", false)
+ self:HandleErrors(self.delayed_func, "Delayed string")
+ end
+ self.DelayedString = str
+ if self.UseLua and not canRunLua() then
+ self:SetError("clientside lua is disabled (sv_allowcslua 0)")
+ end
+end
+
+function PART:Initialize()
+ --yield for the compile until other vars are available (UseLua)
+ timer.Simple(0, function()
+ self:SetOnHideString(self:GetOnHideString())
+ self:SetDelayedString(self:GetDelayedString())
+ end)
end
function PART:GetCode()
@@ -58,25 +205,63 @@ function PART:GetNiceName()
if self.UseLua then
return ("lua: " .. self.String)
end
+ if self.String == "" and self.CommandName ~= "" then
+ if self.Action == "Default" then
+ return "pac_event " .. self.CommandName
+ elseif self.Action == "On" then
+ return "pac_event " .. self.CommandName .. " 1"
+ elseif self.Action == "Off" then
+ return "pac_event " .. self.CommandName .. " 0"
+ elseif self.Action == "Toggle" then
+ return "pac_event " .. self.CommandName .. " 2"
+ elseif self.Action == "Sequence+" then
+ return "pac_event_sequenced " .. self.CommandName .. " +"
+ elseif self.Action == "Sequence-" then
+ return "pac_event_sequenced " .. self.CommandName .. " -"
+ elseif self.Action == "SequenceSet" then
+ return "pac_event_sequenced " .. self.CommandName .. " set " .. self.Minimum .. "[bounds:" .. self.Minimum .. ", " .. self.Maximum .."]"
+ elseif self.Action == "Random" then
+ return "pac_event " .. self.CommandName .. " "
+ elseif self.Action == "RandomSet" then
+ return "pac_event_sequenced " .. self.CommandName .. " set " .. ""
+ end
+ end
return "command: " .. self.String
end
-function PART:Execute()
+local function try_lua_exec(self, func)
+ if canRunLua() then
+ if isstring(func) then return end
+ local status, err = pcall(func)
+
+ if not status then
+ self:SetError(err)
+ ErrorNoHalt(err .. "\n")
+ end
+ else
+ local msg = "clientside lua is disabled (sv_allowcslua 0)"
+ self:SetError(msg)
+ pac.Message(tostring(self) .. " - ".. msg)
+ end
+end
+
+function PART:Execute(commandstring)
local ent = self:GetPlayerOwner()
if ent == pac.LocalPlayer then
- if self.UseLua and self.func then
- if canRunLua() then
- local status, err = pcall(self.func)
-
- if not status then
- self:SetError(err)
- ErrorNoHalt(err .. "\n")
+ if self.UseLua then
+ if (self.func or self.onhide_func or self.delayed_func) then
+ if commandstring == nil then --regular string
+ try_lua_exec(self, self.func)
+ else --other modes
+ if ((commandstring == self.OnHideString) and self.onhide_func) then
+ try_lua_exec(self, self.onhide_func)
+ elseif ((commandstring == self.DelayedString) and self.delayed_func) then
+ try_lua_exec(self, self.delayed_func)
+ elseif ((commandstring == self.String) and self.func) then
+ try_lua_exec(self, self.func)
+ end
end
- else
- local msg = "clientside lua is disabled (sv_allowcslua 0)"
- self:SetError(msg)
- pac.Message(tostring(self) .. " - ".. msg)
end
else
if hook.Run("PACCanRunConsoleCommand", self.String) == false then return end
@@ -84,9 +269,51 @@ function PART:Execute()
self:SetError("Concommand is blocked")
return
end
- ent:ConCommand(self.String)
+ if self.String == "" and self.CommandName ~= "" then
+ --[[
+ ["Default: single-shot"] = "Default",
+ ["Default: On (1)"] = "On",
+ ["Default: Off (0)"] = "Off",
+ ["Default: Toggle (2)"] = "Toggle",
+ ["Sequence: forward"] = "Sequence+",
+ ["Sequence: back"] = "Sequence-",
+ ["Sequence: set"] = "SequenceSet",
+ ["Random"] = "Random",
+ ["Random"] = "RandomSet",
+ ]]
+ if self.Action == "Default" then
+ ent:ConCommand("pac_event " .. self.CommandName)
+ elseif self.Action == "On" then
+ ent:ConCommand("pac_event " .. self.CommandName .. " 1")
+ elseif self.Action == "Off" then
+ ent:ConCommand("pac_event " .. self.CommandName .. " 0")
+ elseif self.Action == "Toggle" then
+ ent:ConCommand("pac_event " .. self.CommandName .. " 2")
+ elseif self.Action == "Sequence+" then
+ ent:ConCommand("pac_event_sequenced " .. self.CommandName .. " +")
+ elseif self.Action == "Sequence-" then
+ ent:ConCommand("pac_event_sequenced " .. self.CommandName .. " -")
+ elseif self.Action == "SequenceSet" then
+ ent:ConCommand("pac_event_sequenced " .. self.CommandName .. " set " .. self.Minimum)
+ elseif self.Action == "Random" then
+ local randnum = math.floor(math.Rand(self.Minimum, self.Maximum + 1))
+ ent:ConCommand("pac_event " .. self.CommandName .. randnum)
+ elseif self.Action == "RandomSet" then
+ local randnum = math.floor(math.Rand(self.Minimum, self.Maximum + 1))
+ ent:ConCommand("pac_event_sequenced " .. self.CommandName .. " set " .. randnum)
+ end
+ return
+ end
+
+ if self.DynamicMode then
+ ent:ConCommand(self.String .. " " .. self.AppendedNumber)
+ return
+ end
+ ent:ConCommand(commandstring or self.String)
end
end
end
+PART.OnDoubleClickSpecified = PART.Execute
+
BUILDER:Register()
diff --git a/lua/pac3/core/client/parts/damage_zone.lua b/lua/pac3/core/client/parts/damage_zone.lua
new file mode 100644
index 000000000..f54933f4e
--- /dev/null
+++ b/lua/pac3/core/client/parts/damage_zone.lua
@@ -0,0 +1,1267 @@
+local BUILDER, PART = pac.PartTemplate("base_movable")
+
+--ultrakill parryables: club, slash, buckshot
+
+PART.ClassName = "damage_zone"
+PART.Group = "combat"
+PART.Icon = "icon16/package.png"
+
+PART.ImplementsDoubleClickSpecified = true
+
+local renderhooks = {
+ "PostDraw2DSkyBox",
+ "PostDrawOpaqueRenderables",
+ "PostDrawSkyBox",
+ "PostDrawTranslucentRenderables",
+ "PostDrawViewModel",
+ "PostPlayerDraw",
+ "PreDrawEffects",
+ "PreDrawHalos",
+ "PreDrawOpaqueRenderables",
+ "PreDrawSkyBox",
+ "PreDrawTranslucentRenderables",
+ "PreDrawViewModel"
+}
+
+local recycle_hitmark = CreateConVar("pac_damage_zone_recycle_hitmarkers", "0", FCVAR_ARCHIVE, "Whether to use the experimental recycling system to save performance on spawning already created hit markers.\nIf this is 0, it will be more reliable but more costly because it creates new parts every time.")
+
+
+BUILDER:StartStorableVars()
+ :SetPropertyGroup("Targets")
+ :GetSet("AffectSelf",false)
+ :GetSet("Players",true)
+ :GetSet("NPC",true)
+ :GetSet("PointEntities",true, {description = "Other source engine entities such as item_item_crate and prop_physics"})
+ :GetSet("Friendlies", true, {description = "friendly NPCs can be targeted"})
+ :GetSet("Neutrals", true, {description = "neutral NPCs can be targeted"})
+ :GetSet("Hostiles", true, {description = "hostile NPCs can be targeted"})
+ :SetPropertyGroup("Shape and Sampling")
+ :GetSet("Radius", 20, {editor_onchange = function(self,num) return math.floor(math.Clamp(num,-32768,32767)) end})
+ :GetSet("Length", 50, {editor_onchange = function(self,num) return math.floor(math.Clamp(num,-32768,32767)) end})
+ :GetSet("HitboxMode", "Box", {enums = {
+ ["Box"] = "Box",
+ ["Cube"] = "Cube",
+ ["Sphere"] = "Sphere",
+ ["Cylinder (Raycasts Only)"] = "Cylinder",
+ ["Cylinder (Hybrid)"] = "CylinderHybrid",
+ ["Cylinder (From Spheres)"] = "CylinderSpheres",
+ ["Cone (Raycasts Only)"] = "Cone",
+ ["Cone (Hybrid)"] = "ConeHybrid",
+ ["Cone (From Spheres)"] = "ConeSpheres",
+ ["Ray"] = "Ray"
+ }})
+ :GetSet("Detail", 20, {editor_onchange = function(self,num) return math.floor(math.Clamp(num,-32,31)) end})
+ :GetSet("ExtraSteps",0, {editor_onchange = function(self,num) return math.floor(math.Clamp(num,-8,7)) end})
+ :GetSet("RadialRandomize", 1, {editor_onchange = function(self,num) return math.Clamp(num,-8,7) end})
+ :GetSet("PhaseRandomize", 1, {editor_onchange = function(self,num) return math.Clamp(num,-8,7) end})
+ :SetPropertyGroup("Falloff")
+ :GetSet("DamageFalloff", false)
+ :GetSet("DamageFalloffPower", 1, {editor_onchange = function(self,num) return math.Clamp(num,-64,63) end})
+ :SetPropertyGroup("Preview Rendering")
+ :GetSet("Preview", false)
+ :GetSet("RenderingHook", "PostDrawOpaqueRenderables", {enums = {
+ ["PostDraw2DSkyBox"] = "PostDraw2DSkyBox",
+ ["PostDrawOpaqueRenderables"] = "PostDrawOpaqueRenderables",
+ ["PostDrawSkyBox"] = "PostDrawSkyBox",
+ ["PostDrawTranslucentRenderables"] = "PostDrawTranslucentRenderables",
+ ["PostDrawViewModel"] = "PostDrawViewModel",
+ ["PostPlayerDraw"] = "PostPlayerDraw",
+ ["PreDrawEffects"] = "PreDrawEffects",
+ ["PreDrawHalos"] = "PreDrawHalos",
+ ["PreDrawOpaqueRenderables"] = "PreDrawOpaqueRenderables",
+ ["PreDrawSkyBox"] = "PreDrawSkyBox",
+ ["PreDrawTranslucentRenderables"] = "PreDrawTranslucentRenderables",
+ ["PreDrawViewModel"] = "PreDrawViewModel"
+ }})
+ :SetPropertyGroup("DamageInfo")
+ :GetSet("Bullet", false, {description = "Fires a bullet on each target for the added hit decal"})
+ :GetSet("Damage", 0, {editor_onchange = function(self,num) return math.floor(math.Clamp(num,0,268435455)) end})
+ :GetSet("DamageType", "generic", {enums = {
+ generic = 0, --generic damage
+ crush = 1, --caused by physics interaction
+ bullet = 2, --bullet damage
+ slash = 4, --sharp objects, such as manhacks or other npcs attacks
+ burn = 8, --damage from fire
+ vehicle = 16, --hit by a vehicle
+ fall = 32, --fall damage
+ blast = 64, --explosion damage
+ club = 128, --crowbar damage
+ shock = 256, --electrical damage, shows smoke at the damage position
+ sonic = 512, --sonic damage,used by the gargantua and houndeye npcs
+ energybeam = 1024, --laser
+ nevergib = 4096, --don't create gibs
+ alwaysgib = 8192, --always create gibs
+ drown = 16384, --drown damage
+ paralyze = 32768, --same as dmg_poison
+ nervegas = 65536, --neurotoxin damage
+ poison = 131072, --poison damage
+ acid = 1048576, --
+ airboat = 33554432, --airboat gun damage
+ blast_surface = 134217728, --this won't hurt the player underwater
+ buckshot = 536870912, --the pellets fired from a shotgun
+ direct = 268435456, --
+ dissolve = 67108864, --forces the entity to dissolve on death
+ drownrecover = 524288, --damage applied to the player to restore health after drowning
+ physgun = 8388608, --damage done by the gravity gun
+ plasma = 16777216, --
+ prevent_physics_force = 2048, --
+ radiation = 262144, --radiation
+ removenoragdoll = 4194304, --don't create a ragdoll on death
+ slowburn = 2097152, --
+
+ fire = -1, -- ent:Ignite(5)
+
+ -- env_entity_dissolver
+ dissolve_energy = 0,
+ dissolve_heavy_electrical = 1,
+ dissolve_light_electrical = 2,
+ dissolve_core_effect = 3,
+
+ heal = -1,
+ armor = -1,
+ }})
+ :GetSet("DoNotKill",false, {description = "Only damage to as low as critical health;\nOnly heal to as high as critical health\nIn other words, converge to the critical health"})
+ :GetSet("ReverseDoNotKill",false, {description = "Heal only if health is above critical health;\nDamage only if health is below critical health\nIn other words, move away from the critical health"})
+ :GetSet("CriticalHealth",1, {editor_onchange = function(self,num) return math.floor(math.Clamp(num,0,65535)) end})
+ :GetSet("MaxHpScaling", 0, {editor_clamp = {0,1}})
+ :SetPropertyGroup("DamageOverTime")
+ :GetSet("DOTMode", false, {editor_friendly = "DoT mode",
+ description = "Repeats your damage a few times. Subject to serverside convar."})
+ :GetSet("DOTTime", 0, {editor_friendly = "DoT time", editor_clamp = {0,32}, description = "delay between each repeated damage"})
+ :GetSet("DOTCount", 0, {editor_friendly = "DoT count", editor_onchange = function(self,num) return math.floor(math.Clamp(num,0,127)) end, description = "number of repeated damage instances"})
+ :GetSet("NoInitialDOT", false, {description = "Skips the first instance (the instant one) of damage to achieve a delayed damage for example."})
+ :SetPropertyGroup("HitOutcome")
+ :GetSetPart("HitSoundPart")
+ :GetSetPart("KillSoundPart")
+ :GetSetPart("HitMarkerPart")
+ :GetSet("HitMarkerLifetime", 1)
+ :GetSetPart("KillMarkerPart")
+ :GetSet("KillMarkerLifetime", 1)
+ :GetSet("AllowOverlappingHitSounds", false, {description = "If false, then when there are entities killed, do not play the hit sound part at the same time, since the kill sound takes priority"})
+ :GetSet("AllowOverlappingHitMarkers", false, {description = "If false, then for entities killed, do not spawn the hit marker part, since the kill marker takes priority and we don't want an overlap"})
+ :GetSet("RemoveDuplicateHitMarkers", true, {description = "If true, hit markers on an entity will be removed before creating a new one.\nBE WARNED. You still have a limited budget to create hit markers. It will be enforced."})
+ :GetSet("AttachPartsToTargetEntity",false, {description = "hitparts will be applied to the target entity rather than on the floating hitmarker entity\nThis will require pac_sv_damage_zone_allow_ragdoll_hitparts to be set to 1 serverside"})
+ :GetSet("RemoveNPCWeaponsOnKill",false)
+BUILDER:EndStorableVars()
+
+
+
+--[[UNUSED
+--a budget system to prevent mass abuse of hit marker parts
+function CalculateHitMarkerPrice(part)
+ if not part then return end
+
+ if not part.known_hitmarker_size then part.known_hitmarker_size = 2*#util.TableToJSON(part:ToTable()) end
+ return part.known_hitmarker_size
+end
+
+function HasBudget(owner, part)
+ if not owner.pac_dmgzone_hitmarker_budget then
+ owner.pac_dmgzone_hitmarker_budget = 50000 --50kB's worth of pac parts
+ end
+
+ if part then --calculate based on an additional part added
+ --print("budget:" .. string.NiceSize(owner.pac_dmgzone_hitmarker_budget) .. ", cost: " .. string.NiceSize(CalculateHitMarkerPrice(part)))
+ return owner.pac_dmgzone_hitmarker_budget - CalculateHitMarkerPrice(part) > 0
+ else --get result from current state
+ --print("budget:" .. string.NiceSize(owner.pac_dmgzone_hitmarker_budget))
+ return owner.pac_dmgzone_hitmarker_budget > 0
+ end
+end]]
+
+function PART:LaunchAuditAndEnforceSoftBan(amount, reason)
+ if reason == "recursive loop" then
+ self.stop_until = CurTime() + 3600
+ owner.stop_hit_markers_until = CurTime() + 3600
+ Derma_Message("HEY! You know infinite recursive loops are super duper dangerous?")
+ surface.PlaySound("garrysmod/ui_return.wav")
+ return
+ end
+ local owner = self:GetPlayerOwner()
+ if owner ~= LocalPlayer() then return end
+ owner.stop_hit_markers_admonishment_count = owner.stop_hit_markers_admonishment_count or 1
+ owner.stop_hit_markers_admonishment_message_up = false
+ local str_admonishment = "WARNING.\n"
+ str_admonishment = str_admonishment .. "One of your hit marker parts is way too big. It went ".. string.NiceSize(amount) .. " overbudget at ONCE.\n"
+ if self.HitBoxMode ~= "Ray" then
+ if self.Radius > 300 or self.Length > 300 then
+ str_admonishment = str_admonishment .. "Your damage zone is oversized too. Are you purposefully trying to target large numbers of targets?\n"
+ end
+ end
+ str_admonishment = str_admonishment .. owner.stop_hit_markers_admonishment_count .. " warnings so far\n"
+ if owner.stop_hit_markers_admonishment_count > 5 then
+ self.stop_until = CurTime() + 2
+ owner.stop_hit_markers_until = CurTime() + 180 --that's rough but necessary
+ str_admonishment = str_admonishment .. "FIVE TIMES REPEAT OFFENDER. ENJOY YOUR BAN.\n"
+ end
+
+ self:SetWarning("One of your hit marker parts is way too big. It went ".. string.NiceSize(amount) .. " overbudget at ONCE.")
+ timer.Simple(0.5, function() --don't open duplicate windows
+ if not owner.stop_hit_markers_admonishment_message_up then
+ surface.PlaySound("garrysmod/ui_return.wav")
+ Derma_Message(str_admonishment)
+ self:SetError(str_admonishment.."This part will be limited for 3 minutes")
+ owner.stop_hit_markers_admonishment_message_up = true
+ owner.stop_hit_markers_admonishment_count = owner.stop_hit_markers_admonishment_count + 1
+ print(str_admonishment)
+ end
+ end)
+
+end
+
+function PART:ClearBudgetAdmonishmentWarning()
+ self:SetError() self:SetWarning()
+ owner = self:GetPlayerOwner()
+ owner.stop_hit_markers_admonishment_message_up = false
+ owner.stop_hit_markers_until = 0
+end
+
+local global_hitmarker_CSEnt_seed = 0
+
+local spawn_queue = {}
+local tick = 0
+
+--multiple entities targeted + hit marker creating parts and setting up every time = FRAME DROPS
+--so we tried the budget method, it didn't change the fact that it costs a lot.
+
+--next solution:
+--a table of 20 hit part slots to unhide instead of creating parts every time
+--each player commands a table of hitmarker slots
+--each slot has an entry for a hitpart which will be like a pigeon-hole for clientside hitmarker ents to share
+--hitmarker removal will free up the slot
+--[[
+ owner.hitparts[free] = {
+ active = true,
+ specimen_part = FindOrCreateFloatingPart(ent, part_uid),
+ hitmarker_id = ent_id,
+ template_uid = part_uid
+ }
+]]
+
+--add : go up until we find a free spot, register it in the table until the marker is removed and the entry is marked as inactive
+--remove: go up until we find the spot with the same ent id and part uid
+
+
+--hook.Add("Tick", "pac_spawn_hit")
+
+local part_setup_runtimes = 0
+
+--the floating part pool is player-owned
+--uid-indexed for every individual part instance
+--each entry is a table
+--[[
+ {
+ active
+ template_uid --to identify from which part it's derived
+ hitmarker_id --to identify what entity it's attached to
+
+ }
+]]
+--[[
+ owner.hitmarker_partpool[group.UniqueID] = {active, template_uid, group_part_data}
+ owner.hitparts[free] = {active, specimen_part, hitmarker_id, template_uid}
+]]
+
+local must_remove_class = {
+ entity, entity2, player_movement, weapon
+}
+local function CleanupParts(group)
+ for i,part in ipairs(group:GetChildrenList()) do
+ if must_remove_class[part.ClassName] then
+ part:Remove()
+ end
+ end
+end
+
+function PART:FindOrCreateFloatingPart(owner, ent, part_uid, id, parent_ent)
+ owner.hitmarker_partpool = owner.hitmarker_partpool or {}
+ for spec_uid,tbl in pairs(owner.hitmarker_partpool) do
+ if tbl.template_uid == part_uid then
+ if not tbl.active then
+ local part = pac.GetPartFromUniqueID(pac.Hash(owner), spec_uid)
+ local group = part:GetRootPart()
+ group:CallRecursive("Think")
+ return pac.GetPartFromUniqueID(pac.Hash(owner), spec_uid) --whoowee we found an already existing part
+ end
+ end
+ end
+ --what if we don't!
+ local tbl = pac.GetPartFromUniqueID(pac.Hash(owner), part_uid):ToTable()
+ local group = pac.CreatePart("group", owner) --print("\tcreated a group for " .. id)
+
+ group:SetShowInEditor(false)
+
+ local part = pac.CreatePart(tbl.self.ClassName, owner, tbl, tostring(tbl))
+ group:AddChild(part)
+ CleanupParts(group)
+
+ group:CallRecursive("Think")
+ owner.hitmarker_partpool[group.UniqueID] = {player_owner = self:GetPlayerOwner(), active = true, hitmarker_id = id, template_uid = part_uid, group_part_data = group}
+
+ return group, owner.hitmarker_partpool[group.UniqueID]
+
+end
+
+local ragdolls = {}
+
+net.Receive("pac_send_ragdoll", function(len)
+ local entindex = net.ReadUInt(12)
+ local rag = net.ReadEntity()
+ ragdolls[entindex] = rag
+ timer.Simple(2, function() ragdolls[entindex] = nil end)
+end)
+
+local function TryAttachPartToAnEntity(self,group,parent_ent,marker_ent,killing)
+ local can_do_ragdolls = GetConVar("pac_sv_damage_zone_allow_ragdoll_hitparts"):GetBool()
+ if killing and can_do_ragdolls then
+ if isstring(killing) then
+ group:SetOwner(parent_ent)
+ return
+ end
+ end
+ if self.AttachPartsToTargetEntity then
+ --how to determine consent?? dunno I'll add a layer for outfit application consents if I ever implement pac sharing, but pac_sv_prop_outfits works for now
+ if parent_ent:IsPlayer() then
+ if killing and can_do_ragdolls then
+ timer.Simple(0.05, function()
+ local rag = parent_ent:GetRagdollEntity()
+ TryAttachPartToAnEntity(self,group,rag,marker_ent,false)
+ end)
+ return
+ end
+ if GetConVar("pac_sv_prop_outfits"):GetInt() == 2 then
+ group:SetOwnerName(parent_ent:EntIndex())
+ else
+ group:SetOwner(marker_ent)
+ end
+ else
+ if killing and can_do_ragdolls then
+ local ent_index = parent_ent:EntIndex()
+ timer.Simple(0.05, function()
+ rag = ragdolls[ent_index]
+ if IsValid(rag) then
+ rag = rag
+ TryAttachPartToAnEntity(self,group,rag,ent, "ragdoll")
+ end
+ end)
+ return
+ end
+ group:SetOwnerName(parent_ent:EntIndex())
+ end
+
+ else
+ group:SetOwner(marker_ent)
+ end
+end
+
+local function FreeSpotInStack(owner)
+ owner.hitparts = owner.hitparts or {}
+ owner.hitparts_freespots = owner.hitparts_freespots or {}
+ for i=1,50,1 do
+ if owner.hitparts_freespots[i] == nil then owner.hitparts_freespots[i] = false return i end
+ if owner.hitparts_freespots[i] ~= false then
+ if owner.hitparts[i] then
+ if not owner.hitparts[i].active then
+ return i
+ end
+ else
+ return i
+ end
+ end
+ end
+ return nil
+end
+
+--[[
+ owner.hitmarker_partpool[group.UniqueID] = {active, template_uid, group_part_data}
+ owner.hitparts[free] = {active, specimen_part, hitmarker_id, template_uid}
+]]
+
+local function MatchInStack(owner, ent)
+ owner.hitparts = owner.hitparts or {}
+ for i=1,50,1 do
+ if owner.hitparts[i] then
+ if owner.hitparts[i].template_uid == ent.template_uid and owner.hitparts[i].hitmarker_id == ent.marker_id then
+ return i
+ end
+ --match: entry's template uid is the same as entity's template uid
+ --if there's more, still match entry's specimen ID with specimen ID
+ end
+ end
+
+ return nil
+end
+
+local function UIDMatchInStackForExistingPart(owner, ent, part_uid, ent_id)
+ owner.hitparts = owner.hitparts or {}
+ for i=1,50,1 do
+ if owner.hitparts[i] then
+ --print(i, "match compare:", owner.hitparts[i].active, owner.hitparts[i].specimen_part, owner.hitparts[i].hitmarker_id, owner.hitparts[i].template_uid == part_uid)
+ if owner.hitparts[i].template_uid == part_uid then
+ if owner.hitmarker_partpool then
+ for spec_uid,tbl in pairs(owner.hitmarker_partpool) do
+ if tbl.template_uid == part_uid then
+ if not tbl.active then
+ return tbl.group_part_data
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+
+ return nil
+end
+
+--[[
+ owner.hitmarker_partpool[group.UniqueID] = {active, template_uid, group_part_data}
+ owner.hitparts[free] = {active, specimen_part, hitmarker_id, template_uid}
+]]
+function PART:AddHitMarkerToStack(index, owner, ent, part_uid, ent_id, parent_ent, killing)
+ --print("trying to add to stack:")
+ --print("\t\t",owner, ent, part_uid, ent_id, parent_ent)
+ owner.hitparts = owner.hitparts or {}
+ local free = FreeSpotInStack(owner)
+ local returned_part = nil
+ local existingpart = UIDMatchInStackForExistingPart(owner, ent, part_uid, ent_id)
+ returned_part = existingpart
+
+ if free and not existingpart then
+ local group, tbl = self:FindOrCreateFloatingPart(owner, ent, part_uid, ent_id, parent_ent)
+ owner.hitparts[index] = {active = true, specimen_part = group, hitmarker_id = ent_id, template_uid = part_uid, csent = ent, parent_ent = parent_ent}
+ returned_part = owner.hitparts[index].specimen_part
+ TryAttachPartToAnEntity(self,group,parent_ent,ent, killing)
+ else
+ owner.hitparts[index] = {active = true, specimen_part = returned_part, hitmarker_id = ent_id, template_uid = part_uid, csent = ent, parent_ent = parent_ent}
+ TryAttachPartToAnEntity(self,existingpart,parent_ent,ent, killing)
+ end
+
+
+ return returned_part
+end
+
+local function RemoveHitMarker(owner, ent, uid, id)
+ owner.hitparts = owner.hitparts or {}
+
+ local match = MatchInStack(owner, ent)
+ if match then
+ if owner.hitparts[match] then
+ owner.hitparts[match].active = false
+ end
+ end
+ if owner.hitmarker_partpool then
+ for spec_uid,tbl in pairs(owner.hitmarker_partpool) do
+ if tbl.hitmarker_id == id then
+ tbl.active = false
+ tbl.group_part_data:SetHide(true)
+ tbl.group_part_data:SetShowInEditor(false)
+ tbl.group_part_data:SetOwnerName(owner:EntIndex())
+ --print(tbl.group_part_data, "dormant")
+ end
+ end
+ end
+ --SafeRemoveEntity(ent)
+end
+
+--[[
+ owner.hitmarker_partpool[group.UniqueID] = {active, template_uid, group_part_data}
+ owner.hitparts[free] = {active, specimen_part, hitmarker_id, template_uid}
+]]
+function PART:AssignFloatingPartToEntity(index, part, owner, ent, parent_ent, template_uid, marker_id)
+
+ if not IsValid(part) then return false end
+
+ ent.pac_draw_distance = 0
+
+ local group = part
+ local part2 = group:GetChildrenList()[1]
+
+ owner.hitmarker_partpool[group.UniqueID] = {active = true, hitmarker_id = marker_id, template_uid = template_uid, group_part_data = group}
+ owner.hitparts[index] = {active = true, hitmarker_id = marker_id, template_uid = template_uid, specimen_part = group, csent = ent, parent_ent = parent_ent}
+ self.hitmarkers[group.UniqueID] = owner.hitmarker_partpool[group.UniqueID]
+
+ parent_ent.pac_dmgzone_hitmarker_ents = parent_ent.pac_dmgzone_hitmarker_ents or {}
+ ent.part = group
+ ent.parent_ent = parent_ent
+ ent.template_uid = template_uid
+ parent_ent.pac_dmgzone_hitmarker_ents[marker_id] = ent
+ ent.marker_id = marker_id
+
+ group:SetShowInEditor(false)
+
+ TryAttachPartToAnEntity(self,group,parent_ent,ent)
+
+
+ timer.Simple(0, function() group:SetHide(false) part2:SetHide(false) group:CallRecursive("Think") group:CallRecursive("CalcShowHide") end)
+
+
+ --print(parent_ent, group:IsHidden(), part2:IsHidden())
+
+ owner.hitparts_freespots[index] = false
+ --print(group, "assigned to " .. marker_id .. " / " .. parent_ent:EntIndex())
+
+end
+
+function PART:ClearHitMarkers()
+ for uid, part in pairs(self.hitmarkers) do
+ if IsValid(part) then part:GetRootOwner():Remove() end
+ end
+ local ply = self:GetPlayerOwner()
+ if ply.hitparts then
+ for i,v in pairs(ply.hitparts) do
+ v.specimen_part:Remove()
+ end
+ end
+ ply.hitmarker_partpool = nil
+ ply.hitparts = nil
+end
+
+local function RecursedHitmarker(part)
+ if part.HitMarkerPart == part or part.KillMarkerPart == part then
+ return true
+ end
+ if IsValid(part.HitMarkerPart) then
+ for i,child in pairs(part.HitMarkerPart:GetChildrenList()) do
+ if child.ClassName == "damage_zone" then
+ if child.HitMarkerPart == part or child.KillMarkerPart == part then
+ return true
+ end
+ end
+ end
+ end
+ if IsValid(part.KillMarkerPart) then
+ for i,child in pairs(part.KillMarkerPart:GetChildrenList()) do
+ if child.ClassName == "damage_zone" then
+ if child.HitMarkerPart == part or child.KillMarkerPart == part then
+ return true
+ end
+ end
+ end
+ end
+
+end
+
+
+--NOT THE ACTUAL DAMAGE TYPES. UNIQUE IDS TO COMPRESS NET MESSAGES
+local damage_ids = {
+ generic = 0, --generic damage
+ crush = 1, --caused by physics interaction
+ bullet = 2, --bullet damage
+ slash = 3, --sharp objects, such as manhacks or other npcs attacks
+ burn = 4, --damage from fire
+ vehicle = 5, --hit by a vehicle
+ fall = 6, --fall damage
+ blast = 7, --explosion damage
+ club = 8, --crowbar damage
+ shock = 9, --electrical damage, shows smoke at the damage position
+ sonic = 10, --sonic damage,used by the gargantua and houndeye npcs
+ energybeam = 11, --laser
+ nevergib = 12, --don't create gibs
+ alwaysgib = 13, --always create gibs
+ drown = 14, --drown damage
+ paralyze = 15, --same as dmg_poison
+ nervegas = 16, --neurotoxin damage
+ poison = 17, --poison damage
+ acid = 18, --
+ airboat = 19, --airboat gun damage
+ blast_surface = 20, --this won't hurt the player underwater
+ buckshot = 21, --the pellets fired from a shotgun
+ direct = 22, --
+ dissolve = 23, --forces the entity to dissolve on death
+ drownrecover = 24, --damage applied to the player to restore health after drowning
+ physgun = 25, --damage done by the gravity gun
+ plasma = 26, --
+ prevent_physics_force = 27, --
+ radiation = 28, --radiation
+ removenoragdoll = 29, --don't create a ragdoll on death
+ slowburn = 30, --
+
+ fire = 31, -- ent:Ignite(5)
+
+ -- env_entity_dissolver
+ dissolve_energy = 32,
+ dissolve_heavy_electrical = 33,
+ dissolve_light_electrical = 34,
+ dissolve_core_effect = 35,
+
+ heal = 36,
+ armor = 37,
+}
+
+local hitbox_ids = {
+ ["Box"] = 1,
+ ["Cube"] = 2,
+ ["Sphere"] = 3,
+ ["Cylinder"] = 4,
+ ["CylinderHybrid"] = 5,
+ ["CylinderSpheres"] = 6,
+ ["Cone"] = 7,
+ ["ConeHybrid"] = 8,
+ ["ConeSpheres"] = 9,
+ ["Ray"] = 10
+}
+
+--the hit results net receiver needs to resolve to the part but UID strings is a bit weighty so partial UID are a compromise
+local part_partialUID_caches = {}
+
+--more compressed net message
+function PART:SendNetMessage()
+ part_partialUID_caches[string.sub(self.UniqueID,0,6)] = self
+ pac.Blocked_Combat_Parts = pac.Blocked_Combat_Parts or {}
+ if pac.LocalPlayer ~= self:GetPlayerOwner() then return end
+ if not GetConVar('pac_sv_damage_zone'):GetBool() then return end
+ if util.NetworkStringToID( "pac_request_zone_damage" ) == 0 then self:SetError("This part is deactivated on the server") return end
+ if pac.Blocked_Combat_Parts then
+ if pac.Blocked_Combat_Parts[self.ClassName] then return end
+ end
+ if not pac.CountNetMessage() then self:SetInfo("Went beyond the allowance") end
+
+ if GetConVar("pac_sv_combat_enforce_netrate_monitor_serverside"):GetBool() then
+ if not pac.CountNetMessage() then self:SetInfo("Went beyond the allowance") return end
+ end
+
+ net.Start("pac_request_zone_damage", true)
+
+ net.WriteVector(self:GetWorldPosition())
+ net.WriteAngle(self:GetWorldAngles())
+ net.WriteUInt(self.Damage, 28)
+ net.WriteUInt(self.MaxHpScaling*1000,10)
+ net.WriteInt(self.Length, 16)
+ net.WriteInt(self.Radius, 16)
+ net.WriteBool(self.AffectSelf)
+ net.WriteBool(self.NPC)
+ net.WriteBool(self.Players)
+ net.WriteBool(self.PointEntities)
+ net.WriteBool(self.Friendlies)
+ net.WriteBool(self.Neutrals)
+ net.WriteBool(self.Hostiles)
+ net.WriteUInt(hitbox_ids[self.HitboxMode] or 1,5)
+ net.WriteUInt(damage_ids[self.DamageType] or 0,7)
+ net.WriteInt(self.Detail,6)
+ net.WriteInt(self.ExtraSteps,4)
+ net.WriteInt(math.floor(math.Clamp(8*self.RadialRandomize,-64, 63)), 7)
+ net.WriteInt(math.floor(math.Clamp(8*self.PhaseRandomize,-64, 63)), 7)
+ net.WriteBool(self.DamageFalloff)
+ net.WriteInt(math.floor(math.Clamp(8*self.DamageFalloffPower,-512, 511)), 12)
+ net.WriteBool(self.Bullet)
+ net.WriteBool(self.DoNotKill)
+ net.WriteBool(self.ReverseDoNotKill)
+ net.WriteUInt(self.CriticalHealth, 16)
+ net.WriteBool(self.RemoveNPCWeaponsOnKill)
+ net.WriteBool(self.DOTMode)
+ net.WriteBool(self.NoInitialDOT)
+ net.WriteUInt(self.DOTCount, 7)
+ net.WriteUInt(math.ceil(math.Clamp(64*self.DOTTime, 0, 2047)), 11)
+ net.WriteString(string.sub(self.UniqueID,0,6))
+ local using_hit_feedback = IsValid(self.HitMarkerPart) or IsValid(self.KillMarkerPart)
+ net.WriteBool(using_hit_feedback)
+ net.SendToServer()
+end
+
+function PART:OnShow()
+ if pace.still_loading_wearing then return end
+ if self.validTime > SysTime() then return end
+
+ if self.Preview then
+ self:PreviewHitbox()
+ end
+ self.stop_until = self.stop_until or 0
+ if self.stop_until then self:GetPlayerOwner().stop_hit_markers_admonishment_message_up = nil end
+ if (self:GetPlayerOwner().stop_hit_markers_admonishment_message_up) or self.stop_until > CurTime() then return end
+
+ if self:GetRootPart():GetOwner() ~= self:GetPlayerOwner() then --dumb workaround for when it activates before it realizes it needs to be hidden first
+ timer.Simple(0.01, function() --wait to check if needs to be hidden first
+ if self:IsHidden() or self:IsDrawHidden() then return end
+ if self.Preview then
+ self:PreviewHitbox()
+ end
+
+ self:SendNetMessage()
+ end)
+ else
+ self:SendNetMessage()
+ end
+end
+
+function PART:OnDoubleClickSpecified()
+ self:SendNetMessage()
+end
+
+local dmgzone_requesting_corpses = {}
+function PART:SetAttachPartsToTargetEntity(b)
+ self.AttachPartsToTargetEntity = b
+ if pac.LocalPlayer ~= self:GetPlayerOwner() then return end
+ if self.KillMarkerPart == nil then return end
+ if b then
+ net.Start("pac_request_ragdoll_sends")
+ net.WriteBool(true)
+ net.SendToServer()
+ dmgzone_requesting_corpses[self] = true
+ else
+ dmgzone_requesting_corpses[self] = nil
+ if table.Count(dmgzone_requesting_corpses) == 0 then
+ net.Start("pac_request_ragdoll_sends")
+ net.WriteBool(false)
+ net.SendToServer()
+ end
+ end
+end
+
+--revertable to projectile part's version which wastes time creating new parts but has less issues
+local_hitmarks = {}
+function PART:LegacyAttachToEntity(part, ent)
+ if not part:IsValid() then return false end
+
+ ent.pac_draw_distance = 0
+
+ local tbl = part:ToTable()
+
+ local group = pac.CreatePart("group", self:GetPlayerOwner())
+ group:SetShowInEditor(false)
+
+ local part_clone = pac.CreatePart(tbl.self.ClassName, self:GetPlayerOwner(), tbl, tostring(tbl))
+ group:AddChild(part_clone)
+
+ group:SetOwner(ent)
+ group.SetOwner = function(s) s.Owner = ent end
+ part_clone:SetHide(false)
+
+ local id = group.Id
+ local owner_id = self:GetPlayerOwnerId()
+ if owner_id then
+ id = id .. owner_id
+ end
+
+ ent:CallOnRemove("pac_hitmarker_" .. id, function() group:Remove() end)
+ group:CallRecursive("Think")
+
+ ent.pac_hitmark_part = group
+ ent.pac_hitmark = self --that's just the launcher though
+
+ return true
+end
+
+net.Receive("pac_hit_results", function(len)
+ local uid = net.ReadString() or ""
+ local self = part_partialUID_caches[uid]
+ if not self then return end
+ local hit = net.ReadBool()
+ if hit then
+ self.dmgzone_hit_done = CurTime()
+ end
+ local kill = net.ReadBool()
+ if kill then
+ self.dmgzone_kill_done = CurTime()
+ end
+ local highest_dmg = net.ReadFloat() or 0
+ --most damagezone won't use hitparts, skip the writetables
+ local do_ents_feedback = net.ReadBool()
+ local ents_hit = {}
+ local ents_kill = {}
+ if do_ents_feedback then
+ ents_hit = net.ReadTable(true)
+ if kill then ents_kill = net.ReadTable(true) end
+ end
+ part_setup_runtimes = 0
+
+ if RecursedHitmarker(self) then
+ self:LaunchAuditAndEnforceSoftBan(nil,"recursive loop")
+ end
+
+ local pos = self:GetWorldPosition()
+ local owner = self:GetPlayerOwner()
+
+ self.lag_risk = table.Count(ents_hit) > 15
+
+ local function ValidSound(part)
+ if part ~= nil then
+ if part.ClassName == "sound" or part.ClassName == "sound2" then
+ return true
+ end
+ end
+ return false
+ end
+ --grabbed the function from projectile.lua
+ --here, we spawn a static hitmarker and the max delay is 8 seconds
+ local function spawn(part, pos, ang, parent_ent, duration, owner, killing)
+ if not IsValid(owner) then return end
+ if part == self then return end --stop infinite feedback loops of using the damagezone as a hitmarker
+ --what if people employ a more roundabout method? CRACKDOWN!
+
+
+ if not recycle_hitmark:GetBool() then
+ local ent = parent_ent
+ local cs_ent = false
+ if not self.AttachPartsToTargetEntity then
+ ent = pac.CreateEntity("models/props_junk/popcan01a.mdl")
+ cs_ent = true
+ ent:SetNoDraw(true)
+ ent:SetPos(pos)
+ end
+ self:LegacyAttachToEntity(killing and self.KillMarkerPart or self.HitMarkerPart, ent)
+
+ timer.Simple(math.Clamp(killing and self.KillMarkerLifetime or self.HitMarkerLifetime, 0, 30), function()
+ if IsValid(ent) then
+ if ent.pac_hitmark_part and ent.pac_hitmark_part:IsValid() then
+ ent.pac_hitmark_part:Remove()
+ end
+
+ if cs_ent then
+ SafeRemoveEntityDelayed(ent, 0.5)
+ end
+ end
+ end)
+ return
+ end
+
+ if not owner.hitparts then owner.hitparts = {} end
+
+ if owner.stop_hit_markers_until then
+ if owner.stop_hit_markers_until > CurTime() then return end
+ end
+ if self.lag_risk and math.random() > 0.5 then return end
+ if not self:IsValid() then return end
+ if not part:IsValid() then return end
+
+
+ local start = SysTime()
+ local ent = pac.CreateEntity("models/props_junk/popcan01a.mdl")
+ if not ent:IsValid() then return end
+ ent.is_pac_hitmarker = true
+ ent:SetNoDraw(true)
+ ent:SetOwner(self:GetPlayerOwner())
+ ent:SetPos(pos)
+ ent:SetAngles(ang)
+ global_hitmarker_CSEnt_seed = global_hitmarker_CSEnt_seed + 1
+ local csent_id = global_hitmarker_CSEnt_seed
+
+ --the spawn order needs to decide whether it can or can't create an ent or part
+
+ local flush = self.RemoveDuplicateHitMarkers
+ if flush then
+ --go through the entity and remove the clientside hitmarkers entities
+ if parent_ent.pac_dmgzone_hitmarker_ents then
+ for id,ent2 in pairs(parent_ent.pac_dmgzone_hitmarker_ents) do
+ if IsValid(ent2) then
+ if ent2.part:IsValid() then
+ ent2.part:SetHide(true)
+ end
+ end
+ end
+ end
+ end
+
+ local free_spot = FreeSpotInStack(owner)
+
+ if free_spot then
+ if part:IsValid() then --self:AttachToEntity(part, ent, parent_ent, global_hitmarker_CSEnt_seed)
+ --print("free spot should be " .. free_spot)
+ local newpart
+ local bool = UIDMatchInStackForExistingPart(owner, ent, part.UniqueID, csent_id)
+ if bool then
+ newpart = bool
+ --print("\tpart is existing")
+ else
+ newpart = self:AddHitMarkerToStack(free_spot, owner, ent, part.UniqueID, csent_id, parent_ent, killing)
+ --print("\tpart should be added")
+ end
+
+ self:AssignFloatingPartToEntity(free_spot, newpart, owner, ent, parent_ent, part.UniqueID, csent_id)
+
+ if self.Preview then MsgC("hitmarker:", bool and Color(0,255,0) or Color(0,200,255), bool and "existing" or "created", " : ", newpart, "\n") end
+ timer.Simple(math.Clamp(duration, 0, 8), function()
+ if ent:IsValid() then
+ if parent_ent.pac_dmgzone_hitmarker_ents then
+ for id,ent2 in pairs(parent_ent.pac_dmgzone_hitmarker_ents) do
+ if IsValid(ent2) then
+ RemoveHitMarker(owner, ent2, part.UniqueID, id)
+ owner.hitparts_freespots[free_spot] = true
+ --SafeRemoveEntity(ent2)
+ end
+ end
+ end
+ end
+ end)
+ end
+ end
+
+ local creation_delta = SysTime() - start
+
+ return creation_delta
+ end
+
+ if hit then
+ --try not to play both sounds at once
+ if ValidSound(self.HitSoundPart) then
+ --if can overlap, always play
+ if self.AllowOverlappingHitSounds then
+ self.HitSoundPart:PlaySound()
+ --if cannot overlap, only play if there's only one entity or if we didn't kill
+ elseif (table.Count(ents_kill) <= 1) or not (kill and ValidSound(self.KillSoundPart)) then
+ self.HitSoundPart:PlaySound()
+ end
+ end
+ if self.HitMarkerPart then
+ for _,ent in ipairs(ents_hit) do
+ if IsValid(ent) then
+ local ang = (ent:GetPos() - pos):Angle()
+ if ents_kill[ent] then
+ if self.AllowOverlappingHitMarkers then
+ part_setup_runtimes = part_setup_runtimes + (spawn(self.HitMarkerPart, ent:WorldSpaceCenter(), ang, ent, self.HitMarkerLifetime, owner) or 0)
+ end
+ else
+ part_setup_runtimes = part_setup_runtimes + (spawn(self.HitMarkerPart, ent:WorldSpaceCenter(), ang, ent, self.HitMarkerLifetime, owner) or 0)
+ end
+ end
+ end
+ end
+ end
+ if kill then
+ self.dmgzone_kill_done = CurTime()
+ if ValidSound(self.KillSoundPart) then
+ self.KillSoundPart:PlaySound()
+ end
+ if self.KillMarkerPart then
+ for _,ent in ipairs(ents_kill) do
+ if IsValid(ent) then
+ local ang = (ent:GetPos() - pos):Angle()
+ part_setup_runtimes = part_setup_runtimes + (spawn(self.KillMarkerPart, ent:WorldSpaceCenter(), ang, ent, self.KillMarkerLifetime, owner, true) or 0)
+ end
+ end
+ end
+ end
+ if self.HitMarkerPart or self.KillMarkerPart then
+ if owner.hitparts then
+ self:SetInfo(table.Count(owner.hitparts) .. " hitmarkers in slot")
+ end
+ end
+end)
+
+concommand.Add("pac_cleanup_damagezone_hitmarks", function()
+ if LocalPlayer().hitparts then
+ for i,v in pairs(LocalPlayer().hitparts) do
+ v.specimen_part:Remove()
+ end
+ end
+
+ LocalPlayer().hitmarker_partpool = nil
+ LocalPlayer().hitparts = nil
+end)
+
+
+function PART:OnHide()
+ pac.RemoveHook(self.RenderingHook, "pace_draw_hitbox"..self.UniqueID)
+ for _,v in pairs(renderhooks) do
+ pac.RemoveHook(v, "pace_draw_hitbox"..self.UniqueID)
+ end
+end
+
+function PART:OnRemove()
+ part_partialUID_caches[string.sub(self.UniqueID,0,6)] = nil
+ pac.RemoveHook(self.RenderingHook, "pace_draw_hitbox")
+ for _,v in pairs(renderhooks) do
+ pac.RemoveHook(v, "pace_draw_hitbox")
+ end
+end
+
+local previousRenderingHook
+
+function PART:PreviewHitbox()
+
+ if previousRenderingHook ~= self.RenderingHook then
+ for _,v in pairs(renderhooks) do
+ pac.RemoveHook(v, "pace_draw_hitbox"..self.UniqueID)
+ end
+ previousRenderingHook = self.RenderingHook
+ end
+
+ if not self.Preview then return end
+
+ pac.AddHook(self.RenderingHook, "pace_draw_hitbox"..self.UniqueID, function()
+ if not self.Preview then pac.RemoveHook(self.RenderingHook, "pace_draw_hitbox"..self.UniqueID) end
+ if not IsValid(self) then pac.RemoveHook(self.RenderingHook, "pace_draw_hitbox"..self.UniqueID) end
+ self:GetWorldPosition()
+ if self.HitboxMode == "Box" then
+ local mins = Vector(-self.Radius, -self.Radius, -self.Length)
+ local maxs = Vector(self.Radius, self.Radius, self.Length)
+ render.DrawWireframeBox( self:GetWorldPosition(), Angle(0,0,0), mins, maxs, Color( 255, 255, 255 ) )
+ elseif self.HitboxMode == "Cube" then
+ --mat:Rotate(Angle(SysTime()*100,0,0))
+ local mins = Vector(-self.Radius, -self.Radius, -self.Radius)
+ local maxs = Vector(self.Radius, self.Radius, self.Radius)
+ render.DrawWireframeBox( self:GetWorldPosition(), Angle(0,0,0), mins, maxs, Color( 255, 255, 255 ) )
+ elseif self.HitboxMode == "Sphere" then
+ render.DrawWireframeSphere( self:GetWorldPosition(), self.Radius, 10, 10, Color( 255, 255, 255 ) )
+ elseif self.HitboxMode == "Cylinder" or self.HitboxMode == "CylinderHybrid" then
+ local obj = Mesh()
+ self:BuildCylinder(obj)
+ render.SetMaterial( Material( "models/wireframe" ) )
+ mat = Matrix()
+ mat:Translate(self:GetWorldPosition())
+ mat:Rotate(self:GetWorldAngles())
+ cam.PushModelMatrix( mat )
+ obj:Draw()
+ cam.PopModelMatrix()
+ if LocalPlayer() == self:GetPlayerOwner() then
+ if self.Radius ~= 0 then
+ local sides = self.Detail
+ if self.Detail < 1 then sides = 1 end
+
+ local area_factor = self.Radius*self.Radius / (400 + 100*self.Length/math.max(self.Radius,0.1)) --bigger radius means more rays needed to cast to approximate the cylinder detection
+ local steps = 3 + math.ceil(4*(area_factor / ((4 + self.Length/4) / (20 / math.max(self.Detail,1)))))
+ if self.HitboxMode == "CylinderHybrid" and self.Length ~= 0 then
+ area_factor = 0.15*area_factor
+ steps = 1 + math.ceil(4*(area_factor / ((4 + self.Length/4) / (20 / math.max(self.Detail,1)))))
+ end
+ steps = math.max(steps + math.abs(self.ExtraSteps),1)
+
+ --print("steps",steps, "total casts will be "..steps*self.Detail)
+ for ringnumber=1,0,-1/steps do --concentric circles go smaller and smaller by lowering the i multiplier
+ phase = math.random()
+ for i=1,0,-1/sides do
+ if ringnumber == 0 then i = 0 end
+ x = self:GetWorldAngles():Right()*math.cos(2 * math.pi * i + phase * self.PhaseRandomize)*self.Radius*ringnumber*(1 - math.random() * (ringnumber) * self.RadialRandomize)
+ y = self:GetWorldAngles():Up() *math.sin(2 * math.pi * i + phase * self.PhaseRandomize)*self.Radius*ringnumber*(1 - math.random() * (ringnumber) * self.RadialRandomize)
+ local startpos = self:GetWorldPosition() + x + y
+ local endpos = self:GetWorldPosition() + self:GetWorldAngles():Forward()*self.Length + x + y
+ render.DrawLine( startpos, endpos, Color( 255, 255, 255 ), false )
+ end
+ end
+ if self.HitboxMode == "CylinderHybrid" and self.Length ~= 0 then
+ --fast sphere check on the wide end
+ if self.Length/self.Radius >= 2 then
+ render.DrawWireframeSphere( self:GetWorldPosition() + self:GetWorldAngles():Forward()*(self.Length - self.Radius), self.Radius, 10, 10, Color( 255, 255, 255 ) )
+ render.DrawWireframeSphere( self:GetWorldPosition() + self:GetWorldAngles():Forward()*(self.Radius), self.Radius, 10, 10, Color( 255, 255, 255 ) )
+ if self.Radius ~= 0 then
+ local counter = 0
+ for i=math.floor(self.Length / self.Radius) - 1,1,-1 do
+ render.DrawWireframeSphere( self:GetWorldPosition() + self:GetWorldAngles():Forward()*(self.Radius*i), self.Radius, 10, 10, Color( 255, 255, 255 ) )
+ if counter == 100 then break end
+ counter = counter + 1
+ end
+ end
+ --render.DrawWireframeSphere( self:GetWorldPosition() + self:GetWorldAngles():Forward()*(self.Length - 0.5*self.Radius), 0.5*self.Radius, 10, 10, Color( 255, 255, 255 ) )
+ end
+ end
+ elseif self.Radius == 0 then render.DrawLine( self:GetWorldPosition(), self:GetWorldPosition() + self:GetWorldAngles():Forward()*self.Length, Color( 255, 255, 255 ), false ) end
+ end
+ elseif self.HitboxMode == "CylinderSpheres" then
+ local obj = Mesh()
+ self:BuildCylinder(obj)
+ render.SetMaterial( Material( "models/wireframe" ) )
+ mat = Matrix()
+ mat:Translate(self:GetWorldPosition())
+ mat:Rotate(self:GetWorldAngles())
+ cam.PushModelMatrix( mat )
+ obj:Draw()
+ cam.PopModelMatrix()
+ if self.Length ~= 0 and self.Radius ~= 0 then
+ local counter = 0
+ --render.DrawWireframeSphere( self:GetWorldPosition(), self.Radius, 10, 10, Color( 255, 255, 255 ) )
+ for i=0,1,1/(math.abs(self.Length/self.Radius)) do
+ render.DrawWireframeSphere( self:GetWorldPosition() + self:GetWorldAngles():Forward()*self.Length*i, self.Radius, 10, 10, Color( 255, 255, 255 ) )
+ if counter == 200 then break end
+ counter = counter + 1
+ end
+ render.DrawWireframeSphere( self:GetWorldPosition() + self:GetWorldAngles():Forward()*(self.Length), self.Radius, 10, 10, Color( 255, 255, 255 ) )
+ elseif self.Radius == 0 then
+ render.DrawLine( self:GetWorldPosition(), self:GetWorldPosition() + self:GetWorldAngles():Forward()*self.Length, Color( 255, 255, 255 ), false )
+ end
+ elseif self.HitboxMode == "Cone" or self.HitboxMode == "ConeHybrid" then
+ local obj = Mesh()
+ self:BuildCone(obj)
+ render.SetMaterial( Material( "models/wireframe" ) )
+ mat = Matrix()
+ mat:Translate(self:GetWorldPosition())
+ mat:Rotate(self:GetWorldAngles())
+ cam.PushModelMatrix( mat )
+ obj:Draw()
+ cam.PopModelMatrix()
+ if LocalPlayer() == self:GetPlayerOwner() then
+ if self.Radius ~= 0 then
+ local sides = self.Detail
+ if self.Detail < 1 then sides = 1 end
+ local startpos = self:GetWorldPosition()
+ local area_factor = self.Radius*self.Radius / (400 + 100*self.Length/math.max(self.Radius,0.1)) --bigger radius means more rays needed to cast to approximate the cylinder detection
+ local steps = 3 + math.ceil(4*(area_factor / ((4 + self.Length/4) / (20 / math.max(self.Detail,1)))))
+ if self.HitboxMode == "ConeHybrid" and self.Length ~= 0 then
+ area_factor = 0.15*area_factor
+ steps = 1 + math.ceil(4*(area_factor / ((4 + self.Length/4) / (20 / math.max(self.Detail,1)))))
+ end
+ steps = math.max(steps + math.abs(self.ExtraSteps),1)
+
+ --print("steps",steps, "total casts will be "..steps*self.Detail)
+ for ringnumber=1,0,-1/steps do --concentric circles go smaller and smaller by lowering the i multiplier
+ phase = math.random()
+ local ray_thickness = math.Clamp(0.5*math.log(self.Radius) + 0.05*self.Radius,0,10)*(1.5 - 0.7*ringnumber)
+ for i=1,0,-1/sides do
+ if ringnumber == 0 then i = 0 end
+ x = self:GetWorldAngles():Right()*math.cos(2 * math.pi * i + phase * self.PhaseRandomize)*self.Radius*ringnumber*(1 - math.random() * (ringnumber) * self.RadialRandomize)
+ y = self:GetWorldAngles():Up() *math.sin(2 * math.pi * i + phase * self.PhaseRandomize)*self.Radius*ringnumber*(1 - math.random() * (ringnumber) * self.RadialRandomize)
+ local endpos = self:GetWorldPosition() + self:GetWorldAngles():Forward()*self.Length + x + y
+ render.DrawLine( startpos, endpos, Color( 255, 255, 255 ), false )
+ end
+ --[[render.DrawWireframeBox(self:GetWorldPosition() + self:GetWorldAngles():Forward()*self.Length + self:GetWorldAngles():Right() * self.Radius * ringnumber, Angle(0,0,0),
+ Vector(ray_thickness,ray_thickness,ray_thickness),
+ Vector(-ray_thickness,-ray_thickness,-ray_thickness),
+ Color(255,255,255))]]
+ end
+ if self.HitboxMode == "ConeHybrid" and self.Length ~= 0 then
+ --fast sphere check on the wide end
+ local radius_multiplier = math.atan(math.abs(self.Length/self.Radius)) / (1.5 + 0.1*math.sqrt(self.Length/self.Radius))
+ if self.Length/self.Radius > 0.5 then
+ render.DrawWireframeSphere( self:GetWorldPosition() + self:GetWorldAngles():Forward()*(self.Length - self.Radius * radius_multiplier), self.Radius * radius_multiplier, 10, 10, Color( 255, 255, 255 ) )
+ --render.DrawWireframeSphere( self:GetWorldPosition() + self:GetWorldAngles():Forward()*(self.Length - 0.5*self.Radius), 0.5*self.Radius, 10, 10, Color( 255, 255, 255 ) )
+ end
+ end
+ elseif self.Radius == 0 then
+ render.DrawLine( self:GetWorldPosition(), self:GetWorldPosition() + self:GetWorldAngles():Forward()*self.Length, Color( 255, 255, 255 ), false )
+ end
+ end
+ elseif self.HitboxMode == "ConeSpheres" then
+ local obj = Mesh()
+ self:BuildCone(obj)
+ render.SetMaterial( Material( "models/wireframe" ) )
+ mat = Matrix()
+ mat:Translate(self:GetWorldPosition())
+ mat:Rotate(self:GetWorldAngles())
+ cam.PushModelMatrix( mat )
+ obj:Draw()
+ cam.PopModelMatrix()
+ if self.Radius ~= 0 then
+ local steps
+ steps = math.Clamp(4*math.ceil(self.Length / (self.Radius or 1)),1,50)
+ for i = 1,0,-1/steps do
+ render.DrawWireframeSphere( self:GetWorldPosition() + self:GetWorldAngles():Forward()*self.Length*i, i * self.Radius, 10, 10, Color( 255, 255, 255 ) )
+ end
+
+ steps = math.Clamp(math.ceil(self.Length / (self.Radius or 1)),1,4)
+ for i = 0,1/8,1/128 do
+ render.DrawWireframeSphere( self:GetWorldPosition() + self:GetWorldAngles():Forward()*self.Length*i, i * self.Radius, 10, 10, Color( 255, 255, 255 ) )
+ end
+ elseif self.Radius == 0 then
+ render.DrawLine( self:GetWorldPosition(), self:GetWorldPosition() + self:GetWorldAngles():Forward()*self.Length, Color( 255, 255, 255 ), false )
+ end
+ elseif self.HitboxMode == "Ray" then
+ render.DrawLine( self:GetWorldPosition(), self:GetWorldPosition() + self:GetWorldAngles():Forward()*self.Length, Color( 255, 255, 255 ), false )
+ end
+ end)
+end
+
+function PART:OnThink()
+ if self.Preview then self:PreviewHitbox() end
+end
+
+function PART:BuildCylinder(obj)
+ local sides = 30
+ local circle_tris = {}
+ for i=1,sides,1 do
+ local vert1 = {pos = Vector(0, self.Radius*math.sin((i-1)*(2*math.pi / sides)),self.Radius*math.cos((i-1)*(2*math.pi / sides))), u = 0, v = 0 }
+ local vert2 = {pos = Vector(0, self.Radius*math.sin((i-0)*(2*math.pi / sides)),self.Radius*math.cos((i-0)*(2*math.pi / sides))), u = 0, v = 0 }
+ local vert3 = {pos = Vector(self.Length,self.Radius*math.sin((i-1)*(2*math.pi / sides)),self.Radius*math.cos((i-1)*(2*math.pi / sides))), u = 0, v = 0 }
+ local vert4 = {pos = Vector(self.Length,self.Radius*math.sin((i-0)*(2*math.pi / sides)),self.Radius*math.cos((i-0)*(2*math.pi / sides))), u = 0, v = 0 }
+ --print(vert1.pos,vert3.pos,vert2.pos,vert4.pos)
+ --{vert1,vert2,vert3}
+ --{vert4,vert3,vert2}
+ table.insert(circle_tris, vert1)
+ table.insert(circle_tris, vert2)
+ table.insert(circle_tris, vert3)
+
+ table.insert(circle_tris, vert3)
+ table.insert(circle_tris, vert2)
+ table.insert(circle_tris, vert1)
+
+ table.insert(circle_tris, vert4)
+ table.insert(circle_tris, vert3)
+ table.insert(circle_tris, vert2)
+
+ table.insert(circle_tris, vert2)
+ table.insert(circle_tris, vert3)
+ table.insert(circle_tris, vert4)
+
+ --circle_tris[8*(i-1) + 1] = vert1
+ --circle_tris[8*(i-1) + 2] = vert2
+ --circle_tris[8*(i-1) + 3] = vert3
+ --circle_tris[8*(i-1) + 4] = vert4
+ --circle_tris[8*(i-1) + 5] = vert3
+ --circle_tris[8*(i-1) + 6] = vert2
+ end
+ obj:BuildFromTriangles( circle_tris )
+end
+
+function PART:BuildCone(obj)
+ local sides = 30
+ local circle_tris = {}
+ local verttip = {pos = Vector(0,0,0), u = 0, v = 0 }
+ for i=1,sides,1 do
+ local vert1 = {pos = Vector(self.Length,self.Radius*math.sin((i-1)*(2*math.pi / sides)),self.Radius*math.cos((i-1)*(2*math.pi / sides))), u = 0, v = 0 }
+ local vert2 = {pos = Vector(self.Length,self.Radius*math.sin((i-0)*(2*math.pi / sides)),self.Radius*math.cos((i-0)*(2*math.pi / sides))), u = 0, v = 0 }
+ --print(vert1.pos,vert3.pos,vert2.pos,vert4.pos)
+ --{vert1,vert2,vert3}
+ --{vert4,vert3,vert2}
+ table.insert(circle_tris, verttip)
+ table.insert(circle_tris, vert1)
+ table.insert(circle_tris, vert2)
+
+ table.insert(circle_tris, vert2)
+ table.insert(circle_tris, vert1)
+ table.insert(circle_tris, verttip)
+
+ --circle_tris[8*(i-1) + 1] = vert1
+ --circle_tris[8*(i-1) + 2] = vert2
+ --circle_tris[8*(i-1) + 3] = vert3
+ --circle_tris[8*(i-1) + 4] = vert4
+ --circle_tris[8*(i-1) + 5] = vert3
+ --circle_tris[8*(i-1) + 6] = vert2
+ end
+ obj:BuildFromTriangles( circle_tris )
+end
+
+function PART:Initialize()
+ self.hitmarkers = {}
+ if not GetConVar("pac_sv_damage_zone"):GetBool() or pac.Blocked_Combat_Parts[self.ClassName] then self:SetError("damage zones are disabled on this server!") end
+ self.validTime = SysTime() + 5 --jank fix to try to stop activation on load
+ timer.Simple(0.1, function() --jank fix on the jank fix to allow it earlier on projectiles and hitmarkers
+ local ent = self:GetRootPart():GetOwner()
+ if IsValid(ent) then
+ if ent.is_pac_hitmarker or ent.pac_projectile_part then
+ self.validTime = 0
+ end
+ end
+ end)
+
+end
+
+function PART:SetRadius(val)
+ self.Radius = val
+ local sv_dist = GetConVar("pac_sv_damage_zone_max_radius"):GetInt()
+ if self.Radius > sv_dist then
+ self:SetInfo("Your radius is beyond the server's maximum permitted! Server max is " .. sv_dist)
+ else
+ self:SetInfo(nil)
+ end
+end
+
+function PART:SetLength(val)
+ self.Length = val
+ local sv_dist = GetConVar("pac_sv_damage_zone_max_length"):GetInt()
+ if self.Length > sv_dist then
+ self:SetInfo("Your length is beyond the server's maximum permitted! Server max is " .. sv_dist)
+ else
+ self:SetInfo(nil)
+ end
+end
+
+function PART:SetDamage(val)
+ self.Damage = val
+ local sv_max = GetConVar("pac_sv_damage_zone_max_damage"):GetInt()
+ if self.Damage > sv_max then
+ self:SetInfo("Your damage is beyond the server's maximum permitted! Server max is " .. sv_max)
+ else
+ self:SetInfo(nil)
+ end
+end
+
+
+BUILDER:Register()
\ No newline at end of file
diff --git a/lua/pac3/core/client/parts/event.lua b/lua/pac3/core/client/parts/event.lua
index 4673a301a..f7afde25a 100644
--- a/lua/pac3/core/client/parts/event.lua
+++ b/lua/pac3/core/client/parts/event.lua
@@ -1,3 +1,6 @@
+include("pac3/core/client/part_pool.lua")
+--include("pac3/editor/client/parts.lua")
+
local FrameTime = FrameTime
local CurTime = CurTime
local NULL = NULL
@@ -5,6 +8,7 @@ local Vector = Vector
local util = util
local SysTime = SysTime
+
local BUILDER, PART = pac.PartTemplate("base")
PART.ClassName = "event"
@@ -13,6 +17,8 @@ PART.ThinkTime = 0
PART.AlwaysThink = true
PART.Icon = 'icon16/clock.png'
+PART.ImplementsDoubleClickSpecified = true
+
BUILDER:StartStorableVars()
BUILDER:GetSet("Event", "", {enums = function(part)
local output = {}
@@ -24,23 +30,361 @@ BUILDER:StartStorableVars()
end
return output
- end})
- BUILDER:GetSet("Operator", "find simple", {enums = function(part) local tbl = {} for i,v in ipairs(part.Operators) do tbl[v] = v end return tbl end})
- BUILDER:GetSet("Arguments", "", {hidden = true})
- BUILDER:GetSet("Invert", true)
+ end, description = "The type of condition used to determine whether to hide or show parts.\nCommon events are button, command, timer, timerx, is_on_ground, health_lost, is_touching"})
+ BUILDER:GetSet("Operator", "find simple", {enums = function(part) local tbl = {} for i,v in ipairs(part.Operators) do tbl[v] = v end return tbl end, description = "How the event will compare its source data with your reference value. PAC will try automatically pick an appropriate operator based on the event.\n\nfind and find simple searches for a keyword match (applies to text only).\nequal looks for an exact match (applies for text and numbers)\nabove, below etc are number comparators and should be self-explanatory.\nmaybe does a coin flip ignoring everything"})
+ BUILDER:GetSet("Arguments", "", {hidden = false, description = "The internal text representation of the event's arguments, how it gets saved.\nThe dynamic fields access that very same thing, but a text field is useful to review and copy all the arguments at once."})
+ BUILDER:GetSet("Invert", true, {description = "invert: show when condition is met\nuninverted: hide when condition is met"})
BUILDER:GetSet("RootOwner", true)
- BUILDER:GetSet("AffectChildrenOnly", false)
+ BUILDER:GetSet("AffectChildrenOnly", false, {description = "Instead of the parent, the event's children will be affected instead"})
BUILDER:GetSet("ZeroEyePitch", false)
- BUILDER:GetSetPart("TargetPart")
+ BUILDER:GetSetPart("TargetPart", {editor_friendly = "ExternalOriginPart", description = "Only applies to some scale or velocity-related events, picks a different point as a reference for measurement.\nFormerly known as target part. If you remember it, forget this misnomer."})
+ BUILDER:GetSetPart("DestinationPart", {editor_friendly = "TargetedPart", description = "Instead of the parent, targets a single part to show/hide."})
+ BUILDER:GetSet("MultipleTargetParts", "", {description = "Instead of the parent, targets a list of parts to show/hide.\nThe list takes the form of UIDs or names separated by semicolons. You can use bulk select to quickly build the list."})
BUILDER:EndStorableVars()
+PART.Tutorials = {}
+
+local registered_command_event_series = {}
+local event_series_bounds = {}
+
+function PART:OnDoubleClickSpecified()
+ if GetConVar("pac_doubleclick_action_specified"):GetInt() == 1 then
+ self:SetInvert(not self:GetInvert())
+ pace.PopulateProperties(self)
+ return
+ end
+
+ if self.Event == "command" then
+ local cmd, time, hide = self:GetParsedArgumentsForObject(self.Events.command)
+ if time == 0 then --toggling mode
+ pac.LocalPlayer.pac_command_events[cmd] = pac.LocalPlayer.pac_command_events[cmd] or {name = cmd, time = pac.RealTime, on = 0}
+ ----MORE PAC JANK?? SOMETIMES, THE 2 NOTATION DOESN'T CHANGE THE STATE YET
+ if pac.LocalPlayer.pac_command_events[cmd].on == 1 then
+ RunConsoleCommand("pac_event", cmd, "0")
+ else
+ RunConsoleCommand("pac_event", cmd, "1")
+ end
+ else
+ RunConsoleCommand("pac_event", cmd)
+ end
+ elseif self.Event == "is_flashlight_on" then
+ RunConsoleCommand("impulse", "100")
+ elseif self.Event == "timerx" or self.Event == "timerx2" then
+ self.time = nil
+ else
+ self:SetInvert(not self:GetInvert())
+ pace.PopulateProperties(self)
+ end
+end
+
+function PART:register_command_event(str,b)
+ local ply = self:GetPlayerOwner()
+
+ local event = str
+ local flush = b
+
+ local num = tonumber(string.sub(event, string.find(event,"[%d]+$") or 0))
+
+ if string.find(event,"[%d]+$") then
+ event = string.gsub(event,"[%d]+$","")
+ end
+ ply.pac_command_event_sequencebases = ply.pac_command_event_sequencebases or {}
+
+ if flush then
+ ply.pac_command_event_sequencebases[event] = nil
+ return
+ end
+ local data = ply.pac_command_event_sequencebases[event] or {name = event}
+ local min = data.min or 1
+ local max = data.max or 1
+ if num then
+ if num < min then min = num end
+ if num > max then max = num end
+ end
+
+ if min then data.min = min end
+ if max then data.max = max end
+
+ if data and string.find(str,"[%d]+$") then
+ event_series_bounds[event] = event_series_bounds[event] or {}
+ event_series_bounds[event][1] = event_series_bounds[event][1] or min
+ event_series_bounds[event][1] = math.min(event_series_bounds[event][1], min)
+ event_series_bounds[event][2] = event_series_bounds[event][2] or max
+ event_series_bounds[event][2] = math.max(event_series_bounds[event][2], max)
+ else
+ data = {name = event}
+ end
+ timer.Simple(0.3, function()
+ if stop_timer then return end
+ stop_timer = true
+ net.Start("pac_event_define_sequence_bounds")
+ net.WriteTable(event_series_bounds)
+ net.SendToServer()
+ end)
+ timer.Simple(0.5, function() stop_timer = false end)
+end
+
+function PART:fix_event_operator()
+ --check if exists
+ --check class
+ --check current operator
+ --PrintTable(PART.Events[self.Event])
+ if PART.Events[self.Event] then
+ local event_type = PART.Events[self.Event].operator_type
+ if event_type == "number" then
+ if self.Operator == "find" or self.Operator == "find simple" then
+ self.Operator = PART.Events[self.Event].preferred_operator --which depends, it's usually above but we'll have cases where it's best to have below, or equal
+ self:SetInfo("The operator was automatically changed to work with this event type, which handles numbers")
+ end
+
+ elseif event_type == "string" then
+ if self.Operator ~= "find" and self.Operator ~= "find simple" and self.Operator ~= "equal" then
+ self.Operator = PART.Events[self.Event].preferred_operator --find simple
+ self:SetInfo("The operator was automatically changed to work with this event type, which handles strings (text)")
+ end
+ elseif event_type == "mixed" then
+ self:SetInfo("This event is mixed, which means it might have different behaviour with numeric operators or string operators. Some of these are that way because they're using different sources of data at once (e.g. addons' weapons can use different formats for fire modes), and we want to catch the most valid uses possible to fit what the event says")
+ --do nothing but still warn about it being a special complex event
+ elseif event_type == "none" then
+ --do nothing
+ end
+ end
+end
+
+function PART:GetEventTutorialText()
+ if PART.Events[self.Event] then
+ return PART.Events[self.Event].tutorial_explanation or "no tutorial entry was added, probably because this event is self-explanatory"
+ else
+ return "invalid event"
+ end
+end
+
+function PART:GetTutorial(str)
+ if not str then
+ if pace and pace.TUTORIALS then
+ return pace.TUTORIALS.PartInfos[self.ClassName].popup_tutorial
+ end
+ end
+ return self:GetEventTutorialText()
+end
+
+function PART:AttachEditorPopup(str)
+
+ local info_string = str or "no information available"
+ local verbosity = ""
+ if self.Event ~= "" then
+ info_string = self:GetEventTutorialText()
+ --if verbosity == "reference tutorial" or verbosity == "beginner tutorial" then
+ --end
+ end
+ str = info_string or str
+ self:SetupEditorPopup(str, true)
+end
+
+local tracked_events = {
+ damage_zone_hit = true,
+ damage_zone_kill = true,
+ lockpart_grabbing = true
+}
function PART:SetEvent(event)
local reset = (self.Arguments == "") or
(self.Arguments ~= "" and self.Event ~= "" and self.Event ~= event)
+ local owner = self:GetPlayerOwner()
+ timer.Simple(1, function()
+ --caching for some events
+ pac.RegisterPartToCache(owner, "button_events", self, event ~= "button")
+ if tracked_events[event] then
+ pac.LinkSpecialTrackedPartsForEvent(self, owner)
+ end
+ end)
+
+ if (owner == pac.LocalPlayer) and (not pace.processing) then
+ if event == "command" then owner.pac_command_events = owner.pac_command_events or {} end
+ if not self.Events[event] then --invalid event? try another event
+ if #string.Split(event, " ") == 2 then --timerx2
+ local strs = string.Split(event, " ")
+ timer.Simple(0.2, function()
+ if not self.pace_properties or self ~= pace.current_part then return end
+ self:SetEvent("timerx2")
+ self:SetArguments(strs[1] .. "@@" .. strs[2] .. "@@1@@0")
+ pace.PopulateProperties(self)
+ end)
+ return
+ end
+ if isnumber(tonumber(event)) then --timerx
+ timer.Simple(0.2, function()
+ if not self.pace_properties or self ~= pace.current_part then return end
+ self:SetEvent("timerx")
+ self:SetArguments(event .. "@@1@@0")
+ pace.PopulateProperties(self)
+ end)
+ return
+ elseif pac.key_enums_reverse[event] then --button
+ timer.Simple(0.2, function()
+ if not self.pace_properties or self ~= pace.current_part then return end
+ self:SetEvent("button")
+ self:SetArguments(event .. "@@0")
+ pace.PopulateProperties(self)
+ end)
+ return
+ else --command
+ if GetConVar("pac_copilot_auto_setup_command_events"):GetBool() then
+ timer.Simple(0.2, function()
+ if not self.pace_properties or self ~= pace.current_part then return end
+ self:SetEvent("command")
+ self:SetArguments(event .. "@@0")
+ pace.PopulateProperties(self)
+ end)
+ return
+ end
+ end
+ end
+ end
+
self.Event = event
self:SetWarning()
- self:GetDynamicProperties(reset)
+ self:SetInfo()
+
+ --foolproofing: fix the operator to match the event's type, and fix arguments as needed
+ self:fix_event_operator()
+ self:fix_args()
+
+ if owner == pac.LocalPlayer then
+ pace.changed_event = self --a reference to make it refresh the popup label panel
+ pace.changed_event_time = CurTime()
+
+ if self == pace.current_part and GetConVar("pac_copilot_make_popup_when_selecting_event"):GetBool() then self:AttachEditorPopup() end --don't flood the popup system with superfluous requests when loading an outfit
+
+ self:GetDynamicProperties(reset)
+ if not GetConVar("pac_editor_remember_divider_height"):GetBool() and IsValid(pace.Editor) then pace.Editor.div:SetTopHeight(ScrH() - 520) end
+
+ end
+end
+
+function PART:SetProperty(key, val)
+ if self["Set" .. key] ~= nil then
+ if self["Get" .. key](self) ~= val then
+ self["Set" .. key](self, val)
+ end
+ elseif self.GetDynamicProperties then
+ local info = self:GetDynamicProperties()[key]
+ if info and info then
+ if isnumber(val) then
+ val = math.Round(val, 7)
+ end
+ info.set(val)
+ if self:GetPlayerOwner() ~= pac.LocalPlayer then return end
+ if pace.IsActive() then
+ if self ~= pace.current_part then return end
+ self.pace_properties["Arguments"]:SetText(" " .. self.Arguments)
+ self.pace_properties["Arguments"].original_str = self.Arguments
+ end
+ end
+ end
+end
+
+function PART:SetArguments(str)
+ self.Arguments = str
+ if pace.IsActive() and pac.LocalPlayer == self:GetPlayerOwner() then
+ if not self:GetShowInEditor() then return end
+ pace.PopulateProperties(self)
+ end
+end
+
+function PART:Initialize()
+ self.showtime = 0
+ self.found_cached_parts = {}
+ self.specialtrackedparts = {}
+ self.ExtraHermites = {}
+ if self:GetPlayerOwner() == LocalPlayer() then
+ timer.Simple(0.2, function()
+ if self.Event == "command" then
+ local cmd, time, hide = self:GetParsedArgumentsForObject(self.Events.command)
+ self:register_command_event(cmd, true)
+ timer.Simple(0.2, function()
+ self:register_command_event(cmd, false)
+ end)
+ end
+ end)
+ end
+ --force refresh
+ timer.Simple(10, function()
+ self.found_cached_parts = {}
+ end)
+
+end
+
+function PART:GetOrFindCachedPart(uid_or_name)
+ local part = nil
+ local existing_part = self.found_cached_parts[uid_or_name]
+ if existing_part then
+ if existing_part ~= NULL and existing_part:IsValid() then
+ return existing_part
+ end
+ end
+
+ local owner = self:GetPlayerOwner()
+ part = pac.GetPartFromUniqueID(pac.Hash(owner), uid_or_name) or pac.FindPartByPartialUniqueID(pac.Hash(owner), uid_or_name)
+ if not part:IsValid() then
+ part = pac.FindPartByName(pac.Hash(owner), uid_or_name, self)
+ else
+ self.found_cached_parts[uid_or_name] = part
+ return part
+ end
+ if part:IsValid() then
+ self.found_cached_parts[uid_or_name] = part
+ return part
+ end
+ return part
+end
+
+function PART:SetMultipleTargetParts(str)
+ self.MultipleTargetParts = str
+ if str == "" then
+ if self.MultiTargetPart then
+ for _,part2 in ipairs(self.MultiTargetPart) do
+ if part2.SetEventTrigger then part2:SetEventTrigger(self, false) end
+ end
+ end
+ self.MultiTargetPart = nil self.ExtraHermites = nil
+ return
+ end
+ self.MultiTargetPart = {}
+ if not string.find(str, ";") then
+ local part = self:GetOrFindCachedPart(str)
+ if IsValid(part) then
+ self:SetDestinationPart(part)
+ self.MultipleTargetParts = ""
+ pace.PopulateProperties(self)
+ else
+ timer.Simple(3, function()
+ local part = self:GetOrFindCachedPart(str)
+ if part then
+ self:SetDestinationPart(part)
+ self.MultipleTargetParts = ""
+ pace.PopulateProperties(self)
+ end
+ end)
+ end
+ self.MultiTargetPart = nil
+ else
+ --self:SetDestinationPart()
+ self.MultiTargetPart = {}
+ self.ExtraHermites = {}
+ local uid_splits = string.Split(str, ";")
+ for i,uid2 in ipairs(uid_splits) do
+ local part = self:GetOrFindCachedPart(uid2)
+ if not IsValid(part) then
+ timer.Simple(3, function()
+ local part = self:GetOrFindCachedPart(uid2)
+ if part then table.insert(self.MultiTargetPart, part) table.insert(self.ExtraHermites, part) end
+ end)
+ else table.insert(self.MultiTargetPart, part) table.insert(self.ExtraHermites, part) end
+ end
+ self.ExtraHermites_Property = "MultipleTargetParts"
+ end
+
end
local function get_default(typ)
@@ -149,9 +493,92 @@ for k,v in pairs(_G) do
end
end
+local grounds_enums = {
+ ["MAT_ANTLION"] = "65",
+ ["MAT_BLOODYFLESH"] = "66",
+ ["MAT_CONCRETE"] = "67",
+ ["MAT_DIRT"] = "68",
+ ["MAT_EGGSHELL"] = "69",
+ ["MAT_FLESH"] = "70",
+ ["MAT_GRATE"] = "71",
+ ["MAT_ALIENFLESH"] = "72",
+ ["MAT_CLIP"] = "73",
+ ["MAT_SNOW"] = "74",
+ ["MAT_PLASTIC"] = "76",
+ ["MAT_METAL"] = "77",
+ ["MAT_SAND"] = "78",
+ ["MAT_FOLIAGE"] = "79",
+ ["MAT_COMPUTER"] = "80",
+ ["MAT_SLOSH"] = "83",
+ ["MAT_TILE"] = "84",
+ ["MAT_GRASS"] = "85",
+ ["MAT_VENT"] = "86",
+ ["MAT_WOOD"] = "87",
+ ["MAT_DEFAULT"] = "88",
+ ["MAT_GLASS"] = "89",
+ ["MAT_WARPSHIELD"] = "90"
+}
+
+local grounds_enums_reverse = {
+ ["65"] = "antlion",
+ ["66"] = "bloody flesh",
+ ["67"] = "concrete",
+ ["68"] = "dirt",
+ ["69"] = "egg shell",
+ ["70"] = "flesh",
+ ["71"] = "grate",
+ ["72"] = "alien flesh",
+ ["73"] = "clip",
+ ["74"] = "snow",
+ ["76"] = "plastic",
+ ["77"] = "metal",
+ ["78"] = "sand",
+ ["79"] = "foliage",
+ ["80"] = "computer",
+ ["83"] = "slosh",
+ ["84"] = "tile",
+ ["85"] = "grass",
+ ["86"] = "vent",
+ ["87"] = "wood",
+ ["88"] = "default",
+ ["89"] = "glass",
+ ["90"] = "warp shield"
+}
+
+local animation_event_enums = {
+ "attack primary",
+ "swim",
+ "flinch rightleg",
+ "flinch leftarm",
+ "flinch head",
+ "cancel",
+ "attack secondary",
+ "flinch rightarm",
+ "jump",
+ "snap yaw",
+ "attack grenade",
+ "custom",
+ "cancel reload",
+ "reload loop",
+ "custom gesture sequence",
+ "custom sequence",
+ "spawn",
+ "doublejump",
+ "flinch leftleg",
+ "flinch chest",
+ "die",
+ "reload end",
+ "reload",
+ "custom gesture"
+}
+
+
+
PART.Events = {}
PART.OldEvents = {
+
random = {
+ operator_type = "number", preferred_operator = "above",
arguments = {{compare = "number"}},
callback = function(self, ent, compare)
return self:NumberOperator(math.random(), compare)
@@ -159,6 +586,7 @@ PART.OldEvents = {
},
randint = {
+ operator_type = "number", preferred_operator = "above",
arguments = {{compare = "number"}, {min = "number"}, {max = "number"}},
callback = function(self, ent, compare, min, max)
min = min or 0
@@ -169,6 +597,8 @@ PART.OldEvents = {
},
random_timer = {
+ operator_type = "none",
+ tutorial_explanation = "random_timer picks a number between min and max, waits this amount of seconds,\nthen activates for the amount of seconds from holdtime.\nafter this is over, it picks a new random number and starts waiting again",
arguments = {{min = "number"}, {max = "number"}, {holdtime = "number"}},
callback = function(self, ent, min, max, holdtime)
@@ -203,11 +633,18 @@ PART.OldEvents = {
},
timerx = {
+ operator_type = "number", preferred_operator = "above",
+ tutorial_explanation = "timerx is a stopwatch that counts time since it's shown (hiding and re-showing is an important resetting condition).\nit takes that time and compares it with the duration defined in seconds.\nmeaning it can show things after(above) a delay or until(below) a certain amount of time passes",
arguments = {{seconds = "number"}, {reset_on_hide = "boolean"}, {synced_time = "boolean"}},
+ userdata = {
+ {default = 0, timerx_property = "seconds"},
+ {default = true, timerx_property = "reset_on_hide"}
+ },
nice = function(self, ent, seconds)
return "timerx: " .. ("%.2f"):format(self.number or 0, 2) .. " " .. self:GetOperator() .. " " .. seconds .. " seconds?"
end,
callback = function(self, ent, seconds, reset_on_hide, synced_time)
+
local time = synced_time and CurTime() or RealTime()
self.time = self.time or time
@@ -222,10 +659,43 @@ PART.OldEvents = {
end,
},
+ timerx2 = {
+ operator_type = "none",
+ tutorial_explanation = "timerx2 is a dual timerx, a stopwatch that counts time since it's shown and determines whether it fits within the window defined by StartTime and EndTime",
+ arguments = {{StartTime = "number"}, {EndTime = "number"}, {reset_on_hide = "boolean"}, {synced_time = "boolean"}},
+ userdata = {
+ {default = 0.5, timerx_property = "StartTime"},
+ {default = 1, timerx_property = "EndTime"},
+ {default = true, timerx_property = "reset_on_hide"}
+ },
+ nice = function(self, ent, seconds, seconds2)
+ return "timerx2: " .. ("%.2f"):format(self.number or 0, 2) .. " between " .. seconds .. " and " .. seconds2 .. " seconds"
+ end,
+ callback = function(self, ent, StartTime, EndTime, reset_on_hide, synced_time)
+
+ local time = synced_time and CurTime() or RealTime()
+
+ self.time = self.time or time
+ self.timerx_reset = reset_on_hide
+
+ if self.AffectChildrenOnly and self:IsHiddenBySomethingElse() then
+ return false
+ end
+ self.number = time - self.time
+
+ return self.number > StartTime and self.number < EndTime
+
+ --return self:NumberOperator(self.number, seconds)
+ end,
+ },
+
timersys = {
+ operator_type = "number", preferred_operator = "above",
+ tutorial_explanation = "like timerx, timersys is a stopwatch that counts time (it uses SysTime()) since it's shown (hiding and re-showing is an important resetting condition).\nit takes that time and compares it with the duration defined in seconds.\nmeaning it can show things after(above) a delay or until(below) a certain amount of time passes",
arguments = {{seconds = "number"}, {reset_on_hide = "boolean"}},
callback = function(self, ent, seconds, reset_on_hide)
+
local time = SysTime()
self.time = self.time or time
@@ -239,6 +709,7 @@ PART.OldEvents = {
},
map_name = {
+ operator_type = "string", preferred_operator = "find simple",
arguments = {{find = "string"}},
callback = function(self, ent, find)
return self:StringOperator(game.GetMap(), find)
@@ -246,6 +717,7 @@ PART.OldEvents = {
},
fov = {
+ operator_type = "number", preferred_operator = "above",
arguments = {{fov = "number"}},
callback = function(self, ent, fov)
ent = try_viewmodel(ent)
@@ -258,6 +730,7 @@ PART.OldEvents = {
end,
},
health_lost = {
+ operator_type = "number", preferred_operator = "above",
arguments = {{amount = "number"}},
callback = function(self, ent, amount)
@@ -299,6 +772,7 @@ PART.OldEvents = {
},
holdtype = {
+ operator_type = "string", preferred_operator = "find simple",
arguments = {{find = "string"}},
callback = function(self, ent, find)
ent = try_viewmodel(ent)
@@ -307,9 +781,18 @@ PART.OldEvents = {
return true
end
end,
+ nice = function(self, ent, find)
+ local str = "holdtype ["..self.Operator.. " " .. find .. "] | "
+ local wep = ent.GetActiveWeapon and ent:GetActiveWeapon() or NULL
+ if wep:IsValid() then
+ str = str .. wep:GetHoldType()
+ end
+ return str
+ end
},
is_crouching = {
+ operator_type = "none",
callback = function(self, ent)
ent = try_viewmodel(ent)
return ent.Crouching and ent:Crouching()
@@ -317,6 +800,7 @@ PART.OldEvents = {
},
is_typing = {
+ operator_type = "none",
callback = function(self, ent)
ent = self:GetPlayerOwner()
return ent.IsTyping and ent:IsTyping()
@@ -324,6 +808,7 @@ PART.OldEvents = {
},
using_physgun = {
+ operator_type = "none",
callback = function(self, ent)
ent = self:GetPlayerOwner()
local pac_drawphysgun_event_part = ent.pac_drawphysgun_event_part
@@ -337,10 +822,13 @@ PART.OldEvents = {
},
eyetrace_entity_class = {
+ operator_type = "string", preferred_operator = "find simple",
+ tutorial_explanation = "this compares the class of the entity you point to with the one(s) written in class",
arguments = {{class = "string"}},
callback = function(self, ent, find)
if ent.GetEyeTrace then
ent = ent:GetEyeTrace().Entity
+ if not IsValid(ent) then return false end
if self:StringOperator(ent:GetClass(), find) then
return true
end
@@ -349,8 +837,10 @@ PART.OldEvents = {
},
owner_health = {
+ operator_type = "number", preferred_operator = "above",
arguments = {{health = "number"}},
callback = function(self, ent, num)
+
ent = try_viewmodel(ent)
if ent.Health then
return self:NumberOperator(ent:Health(), num)
@@ -360,8 +850,10 @@ PART.OldEvents = {
end,
},
owner_max_health = {
+ operator_type = "number", preferred_operator = "above",
arguments = {{health = "number"}},
callback = function(self, ent, num)
+
ent = try_viewmodel(ent)
if ent.GetMaxHealth then
return self:NumberOperator(ent:GetMaxHealth(), num)
@@ -371,6 +863,7 @@ PART.OldEvents = {
end,
},
owner_alive = {
+ operator_type = "none",
callback = function(self, ent)
ent = try_viewmodel(ent)
if ent.Alive then
@@ -380,8 +873,10 @@ PART.OldEvents = {
end,
},
owner_armor = {
+ operator_type = "number", preferred_operator = "above",
arguments = {{armor = "number"}},
callback = function(self, ent, num)
+
ent = try_viewmodel(ent)
if ent.Armor then
return self:NumberOperator(ent:Armor(), num)
@@ -392,31 +887,36 @@ PART.OldEvents = {
},
owner_scale_x = {
+ operator_type = "number", preferred_operator = "above",
arguments = {{scale = "number"}},
callback = function(self, ent, num)
- ent = try_viewmodel(ent)
- return self:NumberOperator(ent.pac_model_scale and ent.pac_model_scale.x or (ent.GetModelScale and ent:GetModelScale()) or 1, num)
+ ent = try_viewmodel(ent)
+ return self:NumberOperator(ent.pac_model_scale and ent.pac_model_scale.x or (ent.GetModelScale and ent:GetModelScale()) or 1, num)
end,
},
owner_scale_y = {
+ operator_type = "number", preferred_operator = "above",
arguments = {{scale = "number"}},
callback = function(self, ent, num)
- ent = try_viewmodel(ent)
- return self:NumberOperator(ent.pac_model_scale and ent.pac_model_scale.y or (ent.GetModelScale and ent:GetModelScale()) or 1, num)
+ ent = try_viewmodel(ent)
+ return self:NumberOperator(ent.pac_model_scale and ent.pac_model_scale.y or (ent.GetModelScale and ent:GetModelScale()) or 1, num)
end,
},
owner_scale_z = {
+ operator_type = "number", preferred_operator = "above",
arguments = {{scale = "number"}},
callback = function(self, ent, num)
- ent = try_viewmodel(ent)
- return self:NumberOperator(ent.pac_model_scale and ent.pac_model_scale.z or (ent.GetModelScale and ent:GetModelScale()) or 1, num)
+ ent = try_viewmodel(ent)
+ return self:NumberOperator(ent.pac_model_scale and ent.pac_model_scale.z or (ent.GetModelScale and ent:GetModelScale()) or 1, num)
end,
},
pose_parameter = {
+ operator_type = "number", preferred_operator = "above",
+ tutorial_explanation = "pose parameters are values used in models for body movement and animation.\nthis event searches a pose parameter and compares its normalized (0-1 range) value with the number defined in num",
arguments = {{name = "string"}, {num = "number"}},
callback = function(self, ent, name, num)
ent = try_viewmodel(ent)
@@ -424,7 +924,23 @@ PART.OldEvents = {
end,
},
+ pose_parameter_true = {
+ operator_type = "number", preferred_operator = "above",
+ tutorial_explanation = "pose parameters are values used in models for body movement and animation.\nthis event searches a pose parameter and compares its true (as opposed to normalized into the 0-1 range) value with number defined in num",
+ arguments = {{name = "string"}, {num = "number"}},
+ callback = function(self, ent, name, num)
+ ent = try_viewmodel(ent)
+ if ent:IsValid() then
+ local min,max = ent:GetPoseParameterRange(ent:LookupPoseParameter(name))
+ if not min or not max then return 0 end
+ local actual_value = min + (max - min)*(ent:GetPoseParameter(name))
+ return self:NumberOperator(actual_value, num)
+ end
+ end,
+ },
+
speed = {
+ operator_type = "number", preferred_operator = "equal",
arguments = {{speed = "number"}},
callback = function(self, ent, num)
ent = try_viewmodel(ent)
@@ -433,6 +949,8 @@ PART.OldEvents = {
},
is_under_water = {
+ operator_type = "number", preferred_operator = "above",
+ tutorial_explanation = "is_under_water activates when you're under a certain level of water.\nas you get deeper, the number is higher.\n0 is dry\n1 is slightly submerged (at least to the feet)\n2 is mostly submerged (at least to the waist)\n3 is completely submerged",
arguments = {{level = "number"}},
callback = function(self, ent, num)
ent = try_viewmodel(ent)
@@ -441,6 +959,7 @@ PART.OldEvents = {
},
is_on_fire = {
+ operator_type = "none",
callback = function(self, ent)
ent = try_viewmodel(ent)
return ent:IsOnFire()
@@ -448,24 +967,80 @@ PART.OldEvents = {
},
client_spawned = {
+ operator_type = "number", preferred_operator = "below",
+ tutorial_explanation = "client_spawned supposedly activates for some time after you spawn",
+
arguments = {{time = "number"}},
callback = function(self, ent, time)
time = time or 0.1
ent = try_viewmodel(ent)
- if ent.pac_playerspawn and ent.pac_playerspawn + time > pac.RealTime then
- return true
+ if ent.pac_playerspawn then
+ return self:NumberOperator(pac.RealTime, ent.pac_playerspawn + time)
end
+ return false
end,
},
is_client = {
+ operator_type = "none",
+ tutorial_explanation = "is_client activates when the group owner entity is your player or viewmodel, rather than another entity like a prop",
callback = function(self, ent)
ent = try_viewmodel(ent)
return self:GetPlayerOwner() == ent
end,
},
+ viewed_by_owner = {
+ operator_type = "none",
+ tutorial = "viewed_by_owner shows for only you. uninvert to show only to other players",
+ callback = function(self, ent)
+ return self:GetPlayerOwner() == pac.LocalPlayer
+ end,
+ },
+
+ seen_by_player = {
+ operator_type = "none",
+ tutorial = "looked_at_by_player activates when a player is looking at you, determined by whether a box around you touches the direct eyeangle line",
+ arguments = {{extra_radius = "number"}, {require_line_of_sight = "boolean"}},
+ userdata = {{editor_panel = "seen_by_player"}},
+ callback = function(self, ent, extra_radius, require_line_of_sight)
+ extra_radius = extra_radius or 0
+ self.nextcheck = self.nextcheck or CurTime() + 0.1
+ if CurTime() > self.nextcheck then
+ for _,v in ipairs(player.GetAll()) do
+ if v == ent then continue end
+ local eyetrace = v:GetEyeTrace()
+
+ if util.IntersectRayWithOBB(eyetrace.StartPos, eyetrace.HitPos - eyetrace.StartPos, LocalPlayer():GetPos() + LocalPlayer():OBBCenter(), Angle(0,0,0), Vector(-extra_radius,-extra_radius,-extra_radius), Vector(extra_radius,extra_radius,extra_radius)) then
+ self.trace_success = true
+ self.trace_success_ply = v
+ self.nextcheck = CurTime() + 0.1
+ goto CHECKOUT
+ end
+ if eyetrace.Entity == ent then
+ self.trace_success = true
+ self.trace_success_ply = v
+ self.nextcheck = CurTime() + 0.1
+ goto CHECKOUT
+ end
+ end
+ self.trace_success = false
+ self.nextcheck = CurTime() + 0.1
+ end
+ ::CHECKOUT::
+ if require_line_of_sight then
+ return self.trace_success
+ and self.trace_success_ply:IsLineOfSightClear(ent) --check world LOS
+ and ((util.QuickTrace(self.trace_success_ply:EyePos(), ent:EyePos() - self.trace_success_ply:EyePos(), self.trace_success_ply).Entity == ent)
+ or (util.QuickTrace(self.trace_success_ply:EyePos(), ent:GetPos() + ent:OBBCenter() - self.trace_success_ply:EyePos(), self.trace_success_ply).Entity == ent))
+ else
+ return self.trace_success
+ end
+ end,
+ },
+
is_flashlight_on = {
+ operator_type = "none",
callback = function(self, ent)
ent = try_viewmodel(ent)
return ent.FlashlightIsOn and ent:FlashlightIsOn()
@@ -473,6 +1048,7 @@ PART.OldEvents = {
},
collide = {
+ operator_type = "none",
callback = function(self, ent)
ent.pac_event_collide_callback = ent.pac_event_collide_callback or ent:AddCallback("PhysicsCollide", function(ent, data)
ent.pac_event_collision_data = data
@@ -489,9 +1065,15 @@ PART.OldEvents = {
},
ranger = {
+ operator_type = "number", preferred_operator = "below",
+ tutorial_explanation = "ranger looks in a line to see if something is in front (red arrow) of its host's (parent) model;\ndetected things could be found before(below) or beyond(above) the distance defined in compare;\nthe event will only look as far as the distance defined in distance",
arguments = {{distance = "number"}, {compare = "number"}, {npcs_and_players_only = "boolean"}},
- userdata = {{editor_panel = "ranger", ranger_property = "distance"}, {editor_panel = "ranger", ranger_property = "compare"}},
+ userdata = {
+ {default = 15, editor_panel = "ranger", ranger_property = "distance"},
+ {default = 5, editor_panel = "ranger", ranger_property = "compare"}
+ },
callback = function(self, ent, distance, compare, npcs_and_players_only)
+
local parent = self:GetParentEx()
if parent:IsValid() and parent.GetWorldPosition then
@@ -516,6 +1098,100 @@ PART.OldEvents = {
self:SetWarning(("ranger doesn't work on [%s] %s"):format(classname, classname ~= name and "(" .. name .. ")" or ""))
end
end,
+ nice = function(self, ent, distance, compare, npcs_and_players_only)
+ local str = "ranger: [" .. self.Operator .. " " .. compare .. "]"
+ return str
+ end
+ },
+
+ ground_surface = {
+ operator_type = "mixed",
+ tutorial_explanation = "ground_surface checks what ground you're standing on, and activates if it matches among the IDs written in surfaces.\nMatch multiple with ;",
+ arguments = {{exclude_noclip = "boolean"}, {surfaces = "string"}},
+ userdata = {{}, {enums = function()
+ --grounds_enums =
+ return
+ {
+ ["MAT_ANTLION"] = "65",
+ ["MAT_BLOODYFLESH"] = "66",
+ ["MAT_CONCRETE"] = "67",
+ ["MAT_DIRT"] = "68",
+ ["MAT_EGGSHELL"] = "69",
+ ["MAT_FLESH"] = "70",
+ ["MAT_GRATE"] = "71",
+ ["MAT_ALIENFLESH"] = "72",
+ ["MAT_CLIP"] = "73",
+ ["MAT_SNOW"] = "74",
+ ["MAT_PLASTIC"] = "76",
+ ["MAT_METAL"] = "77",
+ ["MAT_SAND"] = "78",
+ ["MAT_FOLIAGE"] = "79",
+ ["MAT_COMPUTER"] = "80",
+ ["MAT_SLOSH"] = "83",
+ ["MAT_TILE"] = "84",
+ ["MAT_GRASS"] = "85",
+ ["MAT_VENT"] = "86",
+ ["MAT_WOOD"] = "87",
+ ["MAT_DEFAULT"] = "88",
+ ["MAT_GLASS"] = "89",
+ ["MAT_WARPSHIELD"] = "90"
+ } end}},
+ nice = function(self, ent, exclude_noclip, surfaces)
+ local grounds_enums_reverse = {
+ ["65"] = "antlion",
+ ["66"] = "bloody flesh",
+ ["67"] = "concrete",
+ ["68"] = "dirt",
+ ["69"] = "egg shell",
+ ["70"] = "flesh",
+ ["71"] = "grate",
+ ["72"] = "alien flesh",
+ ["73"] = "clip",
+ ["74"] = "snow",
+ ["76"] = "plastic",
+ ["77"] = "metal",
+ ["78"] = "sand",
+ ["79"] = "foliage",
+ ["80"] = "computer",
+ ["83"] = "slosh",
+ ["84"] = "tile",
+ ["85"] = "grass",
+ ["86"] = "vent",
+ ["87"] = "wood",
+ ["88"] = "default",
+ ["89"] = "glass",
+ ["90"] = "warp shield"
+ }
+ surfaces = surfaces or ""
+ local str = "ground surface: "
+ for i,v in ipairs(string.Split(surfaces,";")) do
+ local element = grounds_enums_reverse[v] or ""
+ str = str .. element
+ if i ~= #string.Split(surfaces,";") then
+ str = str .. ", "
+ end
+ end
+ return str
+ end,
+ callback = function(self, ent, exclude_noclip, surfaces, down)
+ surfaces = surfaces or ""
+ if exclude_noclip and ent:GetMoveType() == MOVETYPE_NOCLIP then return false end
+ local trace = util.TraceLine( {
+ start = self:GetRootPart():GetOwner():GetPos() + Vector( 0, 0, 10),
+ endpos = self:GetRootPart():GetOwner():GetPos() + Vector( 0, 0, -30 ),
+ filter = function(ent)
+ if ent == self:GetRootPart():GetOwner() or ent == self:GetPlayerOwner() then return false end
+ end
+ })
+ local found = false
+ if trace.Hit then
+ local surfs = string.Split(surfaces,";")
+ for _,surf in pairs(surfs) do
+ if surf == tostring(trace.MatType) then found = true end
+ end
+ end
+ return found
+ end
},
is_on_ground = {
@@ -547,10 +1223,15 @@ PART.OldEvents = {
end,
},
+ --this one uses util.TraceHull
is_touching = {
- arguments = {{extra_radius = "number"}},
- userdata = {{editor_panel = "is_touching", is_touching_property = "extra_radius"}},
- callback = function(self, ent, extra_radius)
+ operator_type = "none",
+ tutorial_explanation = "is_touching checks in a box (util.TraceHull) around the host model to see if there's something inside it.\nusually it's the parent model or root owner entity,\nbut you can force it to use the nearest pac3 model as an owner,to override the old root owner setting,\nin case of issues when stacking this event inside other events",
+ arguments = {{extra_radius = "number"}, {nearest_model = "boolean"}},
+ userdata = {{editor_panel = "is_touching", is_touching_property = "extra_radius", default = 0}, {default = 0}},
+ callback = function(self, ent, extra_radius, nearest_model)
+ if nearest_model then ent = self:GetOwner() end
+ if not IsValid(ent) then return false end
extra_radius = extra_radius or 0
local radius = ent:BoundingRadius()
@@ -567,78 +1248,310 @@ PART.OldEvents = {
mins = mins * radius
maxs = maxs * radius
+
local tr = util.TraceHull( {
start = startpos,
endpos = startpos,
maxs = maxs,
mins = mins,
- filter = ent
- } )
+ filter = {ent, self:GetRootPart():GetOwner()}
+ })
+
return tr.Hit
end,
- },
+ nice = function(self, ent, extra_radius, nearest_model)
+ if nearest_model then ent = self:GetOwner() end
+ if not IsValid(ent) then return "" end
+ local radius = ent:BoundingRadius()
- is_in_noclip = {
- callback = function(self, ent)
- ent = try_viewmodel(ent)
- return ent:GetMoveType() == MOVETYPE_NOCLIP and (not ent.GetVehicle or not ent:GetVehicle():IsValid())
- end,
- },
+ if radius == 0 and IsValid(ent.pac_projectile) then
+ radius = ent.pac_projectile:GetRadius()
+ end
- is_voice_chatting = {
- callback = function(self, ent)
- ent = try_viewmodel(ent)
- return ent.IsSpeaking and ent:IsSpeaking()
+ radius = math.Round(math.max(radius + extra_radius + 1, 1))
+
+ local str = self.Event .. " [radius: " .. radius .. "]"
+ return str
end,
},
+ --this one uses ents.FindInBox
+ is_touching_filter = {
+ operator_type = "none",
+ tutorial_explanation = "is_touching_filter checks in a box (ents.FindInBox) around the host model to see if there's something inside it, but you can selectively exclude living things (NPCs or players) from being detected.\nusually the center is the parent model or root owner entity,\nbut you can force it to use the nearest pac3 model as an owner to override the old root owner setting,\nin case of issues when stacking this event inside others",
+ arguments = {{extra_radius = "number"}, {no_npc = "boolean"}, {no_players = "boolean"}, {nearest_model = "boolean"}},
+ userdata = {{editor_panel = "is_touching", is_touching_property = "extra_radius", default = 0}, {default = false}, {default = false}, {default = false}},
+ callback = function(self, ent, extra_radius, no_npc, no_players, nearest_model)
+ if nearest_model then ent = self:GetOwner() end
+ if not IsValid(ent) then return false end
+ extra_radius = extra_radius or 0
+ no_npc = no_npc or false
+ no_players = no_players or false
+ nearest_model = nearest_model or false
- ammo = {
- arguments = {{primary = "boolean"}, {amount = "number"}},
- userdata = {{editor_onchange = function(part, num) return math.Round(num) end}},
- callback = function(self, ent, primary, amount)
- ent = try_viewmodel(ent)
- ent = ent.GetActiveWeapon and ent:GetActiveWeapon() or ent
+ local radius = ent:BoundingRadius()
- if ent:IsValid() then
- return self:NumberOperator(ent.Clip1 and (primary and ent:Clip1() or ent:Clip2()) or 0, amount)
- end
- end,
- },
- total_ammo = {
- arguments = {{ammo_id = "string"}, {amount = "number"}},
- callback = function(self, ent, ammo_id, amount)
- if ent.GetAmmoCount then
- ammo_id = tonumber(ammo_id) or ammo_id:lower()
- if ammo_id == "primary" then
- local wep = ent.GetActiveWeapon and ent:GetActiveWeapon() or NULL
- return self:NumberOperator(wep:IsValid() and ent:GetAmmoCount(wep:GetPrimaryAmmoType()) or 0, amount)
- elseif ammo_id == "secondary" then
- local wep = ent.GetActiveWeapon and ent:GetActiveWeapon() or NULL
- return self:NumberOperator(wep:IsValid() and ent:GetAmmoCount(wep:GetSecondaryAmmoType()) or 0, amount)
- else
- return self:NumberOperator(ent:GetAmmoCount(ammo_id), amount)
- end
+ if radius == 0 and IsValid(ent.pac_projectile) then
+ radius = ent.pac_projectile:GetRadius()
end
- end,
- },
- clipsize = {
- arguments = {{primary = "boolean"}, {amount = "number"}},
- callback = function(self, ent, primary, amount)
- ent = try_viewmodel(ent)
- ent = ent.GetActiveWeapon and ent:GetActiveWeapon() or ent
+ radius = math.max(radius + extra_radius + 1, 1)
- if ent:IsValid() then
- return self:NumberOperator(ent.GetMaxClip1 and (primary and ent:GetMaxClip1() or ent:GetMaxClip2()) or 0, amount)
+ local mins = Vector(-1,-1,-1)
+ local maxs = Vector(1,1,1)
+ local startpos = ent:WorldSpaceCenter()
+ mins = startpos + mins * radius
+ maxs = startpos + maxs * radius
+
+ local b = false
+ local ents_hits = ents.FindInBox(mins, maxs)
+ for _,ent2 in pairs(ents_hits) do
+ if (ent2 ~= ent and ent2 ~= self:GetRootPart():GetOwner()) and
+ (ent2:IsNPC() or ent2:IsPlayer()) and
+ not ( (no_npc and ent2:IsNPC()) or (no_players and ent2:IsPlayer()) )
+ then b = true end
end
+
+ return b
end,
- },
+ nice = function(self, ent, extra_radius, no_npc, no_players, nearest_model)
+ if nearest_model then ent = self:GetOwner() end
+ if not IsValid(ent) then return "" end
+ local radius = ent:BoundingRadius()
- vehicle_class = {
- arguments = {{find = "string"}},
- callback = function(self, ent, find)
- ent = try_viewmodel(ent)
- ent = ent.GetVehicle and ent:GetVehicle() or NULL
+ if radius == 0 and IsValid(ent.pac_projectile) then
+ radius = ent.pac_projectile:GetRadius()
+ end
+
+ radius = math.Round(math.max(radius + extra_radius + 1, 1))
+
+ local str = self.Event .. " [radius: " .. radius .. "]"
+ if no_npc or no_players then str = str .. " | " end
+ if no_npc then str = str .. "no_npc " end
+ if no_players then str = str .. "no_players " end
+ return str
+ end
+ },
+ --this one uses ents.FindInBox
+ is_touching_life = {
+ operator_type = "none",
+ tutorial_explanation = "is_touching_life checks in a stretchable box (ents.FindInBox) around the host model to see if there's something inside it.\nusually the center is the parent model or root owner entity,\nbut you can force it to use the nearest pac3 model as an owner to override the old root owner setting,\nin case of issues when stacking this event inside others",
+
+ arguments = {{extra_radius = "number"}, {x_stretch = "number"}, {y_stretch = "number"}, {z_stretch = "number"}, {no_npc = "boolean"}, {no_players = "boolean"}, {nearest_model = "boolean"}},
+ userdata = {{editor_panel = "is_touching", default = 0}, {x = "x_stretch", default = 1}, {y = "y_stretch", default = 1}, {z = "z_stretch", default = 1}, {default = false}, {default = false}, {default = false}},
+ callback = function(self, ent, extra_radius, x_stretch, y_stretch, z_stretch, no_npc, no_players, nearest_model)
+
+ if nearest_model then ent = self:GetOwner() end
+ if not IsValid(ent) then return false end
+ extra_radius = extra_radius or 0
+ no_npc = no_npc or false
+ no_players = no_players or false
+ x_stretch = x_stretch or 1
+ y_stretch = y_stretch or 1
+ z_stretch = z_stretch or 1
+ nearest_model = nearest_model or false
+
+ local radius = ent:BoundingRadius()
+
+ if radius == 0 and IsValid(ent.pac_projectile) then
+ radius = ent.pac_projectile:GetRadius()
+ end
+
+ radius = math.max(radius + extra_radius + 1, 1)
+
+ local mins = Vector(-x_stretch,-y_stretch,-z_stretch)
+ local maxs = Vector(x_stretch,y_stretch,z_stretch)
+ local startpos = ent:WorldSpaceCenter()
+ mins = startpos + mins * radius
+ maxs = startpos + maxs * radius
+
+ local ents_hits = ents.FindInBox(mins, maxs)
+ local b = false
+ for _,ent2 in pairs(ents_hits) do
+ if IsValid(ent2) and (ent2 ~= ent and ent2 ~= self:GetRootPart():GetOwner()) and
+ (ent2:IsNPC() or ent2:IsPlayer())
+
+ then
+ b = true
+ if ent2:IsNPC() and no_npc then
+ b = false
+ elseif ent2:IsPlayer() and no_players then
+ b = false
+ end
+ if b then return b end
+ end
+ end
+
+ return b
+ end,
+ nice = function(self, ent, extra_radius, x_stretch, y_stretch, z_stretch, no_npc, no_players, nearest_model)
+
+ if nearest_model then ent = self:GetOwner() end
+ if not IsValid(ent) then return "" end
+ local radius = ent:BoundingRadius()
+
+ if radius == 0 and IsValid(ent.pac_projectile) then
+ radius = ent.pac_projectile:GetRadius()
+ end
+
+ radius = math.Round(math.max(radius + extra_radius + 1, 1))
+
+ local str = self.Event .. " [radius: " .. radius .. ", stretch: " .. x_stretch .. "*" .. y_stretch .. "*" .. z_stretch .. "]"
+ if no_npc or no_players then str = str .. " | " end
+ if no_npc then str = str .. "no_npc " end
+ if no_players then str = str .. "no_players " end
+ return str
+ end,
+ },
+ --this one uses util.TraceHull
+ is_touching_scalable = {
+ operator_type = "none",
+ tutorial_explanation = "is_touching_life checks in a stretchable box (util.TraceHull) around the host model to see if there's something inside it.\nusually the center is the parent model or root owner entity,\nbut you can force it to use the nearest pac3 model as an owner to override the old root owner setting,\nin case of issues when stacking this event inside others",
+
+ arguments = {{extra_radius = "number"}, {x_stretch = "number"}, {y_stretch = "number"}, {z_stretch = "number"}, {nearest_model = "boolean"}, {world_only = "boolean"}},
+ userdata = {{editor_panel = "is_touching", default = 15, editor_friendly = "radius"}, {x = "x_stretch", default = 1}, {y = "y_stretch", default = 1}, {z = "z_stretch", default = 1}, {default = false}, {default = false}},
+ callback = function(self, ent, extra_radius, x_stretch, y_stretch, z_stretch, nearest_model, world_only)
+ if nearest_model then ent = self:GetOwner() end
+ if not IsValid(ent) then return false end
+ extra_radius = extra_radius or 15
+ x_stretch = x_stretch or 1
+ y_stretch = y_stretch or 1
+ z_stretch = z_stretch or 1
+ nearest_model = nearest_model or false
+
+ local mins = Vector(-x_stretch,-y_stretch,-z_stretch)
+ local maxs = Vector(x_stretch,y_stretch,z_stretch)
+ local startpos = ent:WorldSpaceCenter()
+
+ radius = math.max(extra_radius, 1)
+ mins = mins * radius
+ maxs = maxs * radius
+
+ if world_only then
+ local tr = util.TraceHull( {
+ start = startpos,
+ endpos = startpos,
+ maxs = maxs,
+ mins = mins,
+ filter = function(ent) return ent:IsWorld() end
+ } )
+ return tr.Hit
+ else
+ local tr = util.TraceHull( {
+ start = startpos,
+ endpos = startpos,
+ maxs = maxs,
+ mins = mins,
+ filter = {self:GetRootPart():GetOwner(),ent}
+ } )
+ return tr.Hit
+ end
+ end,
+ nice = function(self, ent, extra_radius, x_stretch, y_stretch, z_stretch, nearest_model)
+ if nearest_model then ent = self:GetOwner() end
+ if not IsValid(ent) then return "" end
+ local radius = extra_radius
+
+ if radius == 0 and IsValid(ent.pac_projectile) then
+ radius = ent.pac_projectile:GetRadius()
+ end
+
+ radius = math.Round(math.max(extra_radius, 1),1)
+
+ local str = self.Event .. " [radius: " .. radius .. ", stretch: " .. x_stretch .. "*" .. y_stretch .. "*" .. z_stretch .. "]"
+ return str
+ end,
+ },
+
+ is_explicit = {
+ operator_type = "none",
+ tutorial_explanation = "is_explicit activates for viewers who want to hide explicit content with pac_hide_disturbing.\nyou can make special censoring effects for them, for example",
+
+ callback = function(self, ent)
+ return GetConVar("pac_hide_disturbing"):GetBool()
+ end
+ },
+
+ is_in_noclip = {
+ operator_type = "none",
+ callback = function(self, ent)
+ ent = try_viewmodel(ent)
+ return ent:GetMoveType() == MOVETYPE_NOCLIP and (not ent.GetVehicle or not ent:GetVehicle():IsValid())
+ end,
+ },
+
+ is_voice_chatting = {
+ operator_type = "none",
+ callback = function(self, ent)
+ ent = try_viewmodel(ent)
+ return ent.IsSpeaking and ent:IsSpeaking()
+ end,
+ },
+
+ ammo = {
+ operator_type = "number", preferred_operator = "above",
+ tutorial_explanation = "ammo compares the active weapon's current clip ammo on either the primary or secondary ammunition.",
+ arguments = {{primary = "boolean"}, {amount = "number"}},
+ userdata = {{editor_onchange = function(part, num) return math.Round(num) end}},
+ callback = function(self, ent, primary, amount)
+ ent = try_viewmodel(ent)
+ ent = ent.GetActiveWeapon and ent:GetActiveWeapon() or ent
+
+ if ent:IsValid() then
+ return self:NumberOperator(ent.Clip1 and (primary and ent:Clip1() or ent:Clip2()) or 0, amount)
+ end
+ end,
+ },
+ total_ammo = {
+ operator_type = "number", preferred_operator = "above",
+ tutorial_explanation = "total_ammo compares the ammo reserves with a certain amount.\n\nhaving primary or secondary as the ammo ID selects the active weapon.\n\nOtherwise, we expect an ammo ID number.\n\nbeware the ammo IDs are dynamic and might change depending on the server because they're loading different weapons with possible custom ammo.",
+ arguments = {{ammo_id = "string"}, {amount = "number"}},
+ userdata = {{default = "primary", enums = function()
+ local tbl = {}
+ tbl["primary"] = "primary"
+ tbl["secondary"] = "secondary"
+ for i=0,1000,1 do
+ local ammo_name = game.GetAmmoName(i)
+ if ammo_name ~= nil then
+ tbl[ammo_name .. " (ID="..i..")"] = tostring(i)
+ end
+ end
+ return tbl
+ end}},
+ callback = function(self, ent, ammo_id, amount)
+ if ent.GetAmmoCount then
+ ammo_id = tonumber(ammo_id) or ammo_id:lower()
+ if ammo_id == "primary" then
+ local wep = ent.GetActiveWeapon and ent:GetActiveWeapon() or NULL
+ return self:NumberOperator(wep:IsValid() and ent:GetAmmoCount(wep:GetPrimaryAmmoType()) or 0, amount)
+ elseif ammo_id == "secondary" then
+ local wep = ent.GetActiveWeapon and ent:GetActiveWeapon() or NULL
+ return self:NumberOperator(wep:IsValid() and ent:GetAmmoCount(wep:GetSecondaryAmmoType()) or 0, amount)
+ else
+ return self:NumberOperator(ent:GetAmmoCount(ammo_id), amount)
+ end
+ end
+ end,
+ },
+
+ clipsize = {
+ operator_type = "number", preferred_operator = "above",
+ arguments = {{primary = "boolean"}, {amount = "number"}},
+ callback = function(self, ent, primary, amount)
+ ent = try_viewmodel(ent)
+ ent = ent.GetActiveWeapon and ent:GetActiveWeapon() or ent
+
+ if ent:IsValid() then
+ return self:NumberOperator(ent.GetMaxClip1 and (primary and ent:GetMaxClip1() or ent:GetMaxClip2()) or 0, amount)
+ end
+ end,
+ },
+
+ vehicle_class = {
+ operator_type = "string", preferred_operator = "find simple",
+ arguments = {{find = "string"}},
+ callback = function(self, ent, find)
+ ent = try_viewmodel(ent)
+ ent = ent.GetVehicle and ent:GetVehicle() or NULL
if ent:IsValid() then
return self:StringOperator(ent:GetClass(), find)
@@ -647,6 +1560,7 @@ PART.OldEvents = {
},
vehicle_model = {
+ operator_type = "string", preferred_operator = "find simple",
arguments = {{find = "string"}},
callback = function(self, ent, find)
ent = try_viewmodel(ent)
@@ -659,6 +1573,7 @@ PART.OldEvents = {
},
driver_name = {
+ operator_type = "string", preferred_operator = "find simple",
arguments = {{find = "string"}},
callback = function(self, ent, find)
ent = ent.GetDriver and ent:GetDriver() or NULL
@@ -670,6 +1585,7 @@ PART.OldEvents = {
},
entity_class = {
+ operator_type = "string", preferred_operator = "find simple",
arguments = {{find = "string"}},
callback = function(self, ent, find)
return self:StringOperator(ent:GetClass(), find)
@@ -677,6 +1593,7 @@ PART.OldEvents = {
},
weapon_class = {
+ operator_type = "string", preferred_operator = "find simple",
arguments = {{find = "string"}, {hide = "boolean"}},
callback = function(self, ent, find, hide)
ent = try_viewmodel(ent)
@@ -701,6 +1618,7 @@ PART.OldEvents = {
},
has_weapon = {
+ operator_type = "string", preferred_operator = "find simple",
arguments = {{find = "string"}},
callback = function(self, ent, find)
ent = try_viewmodel(ent)
@@ -717,6 +1635,7 @@ PART.OldEvents = {
},
model_name = {
+ operator_type = "string", preferred_operator = "find simple",
arguments = {{find = "string"}},
callback = function(self, ent, find)
return self:StringOperator(ent:GetModel(), find)
@@ -724,9 +1643,14 @@ PART.OldEvents = {
},
sequence_name = {
+ operator_type = "string", preferred_operator = "find simple",
arguments = {{find = "string"}},
- nice = function(self, ent)
- return self.sequence_name or "invalid sequence"
+ nice = function(self, ent, find)
+ local anim = find
+ if find == "" then anim = "" end
+ local str = self.Event .. " ["..self.Operator.. " " .. anim .. "] | "
+ local seq = self.sequence_name or "invalid sequence"
+ return str .. seq
end,
callback = function(self, ent, find)
ent = get_owner(self)
@@ -737,8 +1661,32 @@ PART.OldEvents = {
end,
},
+ ratio_timer = {
+ operator_type = "none",
+ arguments = {{interval = "number"}, {offset = "number"}, {ratio = "number"}, {reset_on_hide = "boolean"}},
+ userdata = {{default = 1}, {default = 0}, {default = 0.5}, {default = false}},
+ callback = function(self, ent, interval, offset, ratio, reset_on_hide)
+ interval = interval or 1
+ offset = offset or 0
+ final_offset = offset
+ ratio = math.Clamp(math.abs(ratio or 0.5), 0, 1)
+
+ if interval == 0 or interval < FrameTime() then
+ self.timer_hack = not self.timer_hack
+ return self.timer_hack
+ end
+
+ if reset_on_hide then
+ final_offset = -self.showtime + offset
+ end
+ return (CurTime() + final_offset) % interval < (interval * ratio)
+ end,
+ },
+
timer = {
+ operator_type = "none",
arguments = {{interval = "number"}, {offset = "number"}},
+ userdata = {{default = 1}, {default = 0}},
callback = function(self, ent, interval, offset)
interval = interval or 1
offset = offset or 0
@@ -753,11 +1701,23 @@ PART.OldEvents = {
},
animation_event = {
- arguments = {{find = "string"}, {time = "number"}},
- nice = function(self)
- return self.anim_name or ""
+ operator_type = "string", preferred_operator = "find simple",
+ arguments = {{find = "string"}, {time = "number"}, {try_stop_gesture = "boolean"}},
+ userdata = {{default = "attack primary", enums = function()
+ local tbl = {}
+ for i,v in pairs(animation_event_enums) do
+ tbl[i] = v
+ end
+ return tbl
+ end}, {default = 0.5}},
+ nice = function(self, ent, find, time)
+ find = find or ""
+ time = time or 0
+ local anim = self.anim_name or ""
+ local str = self.Event .. " ["..self.Operator.. " \"" .. find .. "\" : " .. time .. " seconds] | "
+ return str .. anim
end,
- callback = function(self, ent, find, time)
+ callback = function(self, ent, find, time, try_stop_gesture)
time = time or 0.1
ent = get_owner(self)
@@ -768,6 +1728,15 @@ PART.OldEvents = {
if data and (self:StringOperator(data.name, find) and (time == 0 or data.time + time > pac.RealTime)) then
data.reset = false
b = true
+ if try_stop_gesture then
+ if string.find(find, "attack grenade") then
+ ent:AnimResetGestureSlot( GESTURE_SLOT_GRENADE )
+ elseif string.find(find, "attack") or string.find(find, "reload") then
+ ent:AnimResetGestureSlot( GESTURE_SLOT_ATTACK_AND_RELOAD )
+ elseif string.find(find, "flinch") then
+ ent:AnimResetGestureSlot( GESTURE_SLOT_FLINCH )
+ end
+ end
end
if b then
@@ -781,16 +1750,36 @@ PART.OldEvents = {
},
fire_bullets = {
+ operator_type = "string", preferred_operator = "find simple",
+ tutorial_explanation = "fire_bullets checks what types of bullets you're firing.\nDoesn't seem to work with many addon weapons. The event relies on the FireBullets hook",
arguments = {{find_ammo = "string"}, {time = "number"}},
- callback = function(self, ent, find, time)
+ userdata = {{default = "AR2", enums = function()
+ local tbl = {}
+ for i=-1,512,1 do
+ local name = game.GetAmmoName(i)
+ if name then
+ tbl[name .. " (ID ="..i..")"] = name
+ end
+ end
+ return tbl
+ end}, {default = 0.1}},
+ callback = function(self, ent, find_ammo, time)
time = time or 0.1
ent = try_viewmodel(ent)
+ if game.SinglePlayer() then
+ if self:GetPlayerOwner() == pac.LocalPlayer then
+ if ent.pac_hide_bullets ~= ent:GetNWBool("pac_hide_bullets", false) then
+ net.Start("pac_hide_bullets_get") net.WriteBool(ent.pac_hide_bullets) net.SendToServer()
+ end
+ end
+ end
+
local data = ent.pac_fire_bullets
local b = false
- if data and (self:StringOperator(data.name, find) and (time == 0 or data.time + time > pac.RealTime)) then
+ if data and (self:StringOperator(data.name, find_ammo) and (time == 0 or data.time + time > pac.RealTime)) then
data.reset = false
b = true
end
@@ -800,6 +1789,7 @@ PART.OldEvents = {
},
emit_sound = {
+ operator_type = "string", preferred_operator = "find simple",
arguments = {{find_sound = "string"}, {time = "number"}, {mute = "boolean"}},
callback = function(self, ent, find, time, mute)
time = time or 0.1
@@ -822,19 +1812,43 @@ PART.OldEvents = {
},
command = {
+ operator_type = "string", preferred_operator = "equal",
+ tutorial_explanation = "the command event reads your pac_event states.\nthe pac_event command can turn on (1), off (0) or toggle (2) a state that has a name.\nfor example, \"pac_event myhat 2\" can be used with a myhat command event to put the hat on or off\n\nwith this event, you read the states that contain this find name\n(equal being an exact match; find and find simple allowing to detect from different states having a part of the name)\n\nthe final result is to activate if:\n\tA) there's one active, or \n\tB) there's one recently turned off not too long ago",
arguments = {{find = "string"}, {time = "number"}, {hide_in_eventwheel = "boolean"}},
userdata = {
- {default = "change_me", editor_friendly = "CommandName"},
- {default = 0.1, editor_friendly = "EventDuration"},
+ {default = "change_me", editor_friendly = "CommandName", enums = function()
+ local output = {}
+ local parts = pac.GetLocalParts()
+
+ for i, part in pairs(parts) do
+ if part.ClassName == "command" then
+ local str = part.String
+ if string.find(str,"pac_event") then
+ for s in string.gmatch(str, "pac_event%s[%w_]+") do
+ local name_substring = string.gsub(s,"pac_event%s","")
+ output[name_substring] = name_substring
+ end
+ end
+
+ elseif part.ClassName == "event" and part.Event == "command" then
+ local cmd, time, hide = part:GetParsedArgumentsForObject(part.Events.command)
+ output[cmd] = cmd
+ end
+ end
+
+ return output
+ end},
+ {default = 0, editor_friendly = "EventDuration"},
{default = false, group = "event wheel", editor_friendly = "HideInEventWheel"}
},
nice = function(self, ent, find, time)
find = find or "?"
time = time or "?"
- return "command: " .. find .. " | " .. "duration: " .. time
+ return "command: [" .. self.Operator .. " " .. find .."] | " .. "duration: " .. time
end,
callback = function(self, ent, find, time)
- time = time or 0.1
+
+ time = time or 0
local ply = self:GetPlayerOwner()
@@ -857,6 +1871,8 @@ PART.OldEvents = {
},
say = {
+ operator_type = "string", preferred_operator = "find simple",
+ tutorial_explanation = "say looks at the chat to find if a certain thing has been said some time ago",
arguments = {{find = "string"}, {time = "number"}, {all_players = "boolean"}},
callback = function(self, ent, find, time, all_players)
time = time or 0.1
@@ -886,6 +1902,7 @@ PART.OldEvents = {
-- outfit owner
owner_velocity_length = {
+ operator_type = "number", preferred_operator = "above",
arguments = {{speed = "number"}},
callback = function(self, ent, speed)
local parent = self:GetParentEx()
@@ -899,6 +1916,7 @@ PART.OldEvents = {
end,
},
owner_velocity_forward = {
+ operator_type = "number", preferred_operator = "above",
arguments = {{speed = "number"}},
callback = function(self, ent, speed)
ent = try_viewmodel(ent)
@@ -911,6 +1929,7 @@ PART.OldEvents = {
end,
},
owner_velocity_right = {
+ operator_type = "number", preferred_operator = "above",
arguments = {{speed = "number"}},
callback = function(self, ent, speed)
ent = try_viewmodel(ent)
@@ -923,6 +1942,7 @@ PART.OldEvents = {
end,
},
owner_velocity_up = {
+ operator_type = "number", preferred_operator = "above",
arguments = {{speed = "number"}},
callback = function(self, ent, speed)
ent = try_viewmodel(ent)
@@ -935,6 +1955,7 @@ PART.OldEvents = {
end,
},
owner_velocity_world_forward = {
+ operator_type = "number", preferred_operator = "above",
arguments = {{speed = "number"}},
callback = function(self, ent, speed)
ent = try_viewmodel(ent)
@@ -947,6 +1968,7 @@ PART.OldEvents = {
end,
},
owner_velocity_world_right = {
+ operator_type = "number", preferred_operator = "above",
arguments = {{speed = "number"}},
callback = function(self, ent, speed)
ent = try_viewmodel(ent)
@@ -959,6 +1981,7 @@ PART.OldEvents = {
end,
},
owner_velocity_world_up = {
+ operator_type = "number", preferred_operator = "above",
arguments = {{speed = "number"}},
callback = function(self, ent, speed)
ent = try_viewmodel(ent)
@@ -973,6 +1996,7 @@ PART.OldEvents = {
-- parent part
parent_velocity_length = {
+ operator_type = "number", preferred_operator = "above",
arguments = {{speed = "number"}},
callback = function(self, ent, speed)
local parent = self:GetParentEx()
@@ -989,6 +2013,7 @@ PART.OldEvents = {
end,
},
parent_velocity_forward = {
+ operator_type = "number", preferred_operator = "above",
arguments = {{speed = "number"}},
callback = function(self, ent, speed)
local parent = self:GetParentEx()
@@ -1005,6 +2030,7 @@ PART.OldEvents = {
end,
},
parent_velocity_right = {
+ operator_type = "number", preferred_operator = "above",
arguments = {{speed = "number"}},
callback = function(self, ent, speed)
local parent = self:GetParentEx()
@@ -1021,6 +2047,7 @@ PART.OldEvents = {
end,
},
parent_velocity_up = {
+ operator_type = "number", preferred_operator = "above",
arguments = {{speed = "number"}},
callback = function(self, ent, speed)
local parent = self:GetParentEx()
@@ -1038,48 +2065,108 @@ PART.OldEvents = {
},
parent_scale_x = {
- arguments = {{scale = "number"}},
- callback = function(self, ent, num)
+ operator_type = "number", preferred_operator = "above",
+ arguments = {{scale = "number"},{default_to_grandparent = "boolean"}},
+ userdata = {{default = 1}, {default = true}},
+ callback = function(self, ent, num, default_to_grandparent)
+
local parent = self:GetParentEx()
- if not self.TargetPart:IsValid() and parent:HasParent() then
- parent = parent:GetParent()
+ if default_to_grandparent then --legacy behavior
+ if not self.TargetPart:IsValid() and parent:HasParent() then
+ parent = parent:GetParent()
+ end
+ else
+ --GetParentEx can differ from GetParent, but that only happens if we set TargetPart ("External origin part")
+ if parent ~= self:GetParent() then
+ if self.TargetPart ~= parent then
+ if not self.TargetPart:IsValid() and parent:HasParent() then
+ parent = parent:GetParent()
+ end
+ end
+ end
end
if parent:IsValid() then
- return self:NumberOperator((parent.Type == "part" and parent.Scale and parent.Scale.x * parent.Size) or (parent.pac_model_scale and parent.pac_model_scale.x) or (parent.GetModelScale and parent:GetModelScale()) or 1, num)
+ local value = (parent.Scale and parent.Scale.x * parent.Size)
+ or (parent.pac_model_scale and parent.pac_model_scale.x)
+ or (parent.GetModelScale and parent:GetModelScale())
+ or 1
+ value = math.Round(value,4)
+ self:SetInfo("selected parent : " .. tostring(parent) .. "\nx scale = " .. value)
+ return self:NumberOperator(value, num)
end
return 1
end,
},
parent_scale_y = {
- arguments = {{scale = "number"}},
- callback = function(self, ent, num)
+ operator_type = "number", preferred_operator = "above",
+ arguments = {{scale = "number"},{default_to_grandparent = "boolean"}},
+ userdata = {{default = 1}, {default = true}},
+ callback = function(self, ent, num, default_to_grandparent)
+
local parent = self:GetParentEx()
- if not self.TargetPart:IsValid() and parent:HasParent() then
- parent = parent:GetParent()
+ if default_to_grandparent then --legacy behavior
+ if not self.TargetPart:IsValid() and parent:HasParent() then
+ parent = parent:GetParent()
+ end
+ else
+ --GetParentEx can differ from GetParent, but that only happens if we set TargetPart ("External origin part")
+ if parent ~= self:GetParent() then
+ if self.TargetPart ~= parent then
+ if not self.TargetPart:IsValid() and parent:HasParent() then
+ parent = parent:GetParent()
+ end
+ end
+ end
end
if parent:IsValid() then
- return self:NumberOperator((parent.Type == "part" and parent.Scale and parent.Scale.y * parent.Size) or (parent.pac_model_scale and parent.pac_model_scale.y) or (parent.GetModelScale and parent:GetModelScale()) or 1, num)
+ local value = (parent.Scale and parent.Scale.y * parent.Size)
+ or (parent.pac_model_scale and parent.pac_model_scale.y)
+ or (parent.GetModelScale and parent:GetModelScale())
+ or 1
+ value = math.Round(value,4)
+ self:SetInfo("selected parent : " .. tostring(parent) .. "\ny scale = " .. value)
+ return self:NumberOperator(value, num)
end
return 1
end,
},
parent_scale_z = {
- arguments = {{scale = "number"}},
- callback = function(self, ent, num)
+ operator_type = "number", preferred_operator = "above",
+ arguments = {{scale = "number"},{default_to_grandparent = "boolean"}},
+ userdata = {{default = 1}, {default = true}},
+ callback = function(self, ent, num, default_to_grandparent)
+
local parent = self:GetParentEx()
- if not self.TargetPart:IsValid() and parent:HasParent() then
- parent = parent:GetParent()
+ if default_to_grandparent then --legacy behavior
+ if not self.TargetPart:IsValid() and parent:HasParent() then
+ parent = parent:GetParent()
+ end
+ else
+ --GetParentEx can differ from GetParent, but that only happens if we set TargetPart ("External origin part")
+ if parent ~= self:GetParent() then
+ if self.TargetPart ~= parent then
+ if not self.TargetPart:IsValid() and parent:HasParent() then
+ parent = parent:GetParent()
+ end
+ end
+ end
end
if parent:IsValid() then
- return self:NumberOperator((parent.Type == "part" and parent.Scale and parent.Scale.z * parent.Size) or (parent.pac_model_scale and parent.pac_model_scale.z) or (parent.GetModelScale and parent:GetModelScale()) or 1, num)
+ local value = (parent.Scale and parent.Scale.z * parent.Size)
+ or (parent.pac_model_scale and parent.pac_model_scale.z)
+ or (parent.GetModelScale and parent:GetModelScale())
+ or 1
+ value = math.Round(value,4)
+ self:SetInfo("selected parent : " .. tostring(parent) .. "\nz scale = " .. value)
+ return self:NumberOperator(value, num)
end
return 1
@@ -1087,6 +2174,7 @@ PART.OldEvents = {
},
gravitygun_punt = {
+ operator_type = "number", preferred_operator = "above",
arguments = {{time = "number"}},
callback = function(self, ent, time)
time = time or 0.1
@@ -1095,13 +2183,14 @@ PART.OldEvents = {
local punted = ent.pac_gravgun_punt
- if punted and punted + time > pac.RealTime then
- return true
+ if punted then
+ return self:NumberOperator(pac.RealTime, punted + time)
end
end,
},
movetype = {
+ operator_type = "string", preferred_operator = "find simple",
arguments = {{find = "string"}},
callback = function(self, ent, find)
local mt = ent:GetMoveType()
@@ -1112,6 +2201,8 @@ PART.OldEvents = {
},
dot_forward = {
+ operator_type = "number", preferred_operator = "above",
+ tutorial_explanation = "the dot product is a mathematical operation on vectors (angles / arrows / directions).\n\nfor reference, vectors angled 0 degrees apart have dot of 1, 45 degrees is around 0.707 (half of the square root of 2), 90 degrees is 0,\nand when you go beyond that it goes negative the same way (145 degrees: dot = -0.707, 180 degrees: dot = -1).\n\ndot_forward takes the viewer's eye angles and the root owner's FORWARD component of eye angles;\nmakes the dot product and compares it with the number defined in normal.\nfor example, dot_forward below 0.707 should make something visible if you don't look beyond 45 degrees of the direction of the owner's forward eye angles",
arguments = {{normal = "number"}},
callback = function(self, ent, normal)
@@ -1128,6 +2219,9 @@ PART.OldEvents = {
},
dot_right = {
+ operator_type = "number", preferred_operator = "above",
+ tutorial_explanation = "the dot product is a mathematical operation on vectors (angles / arrows / directions).\n\nfor reference, vectors angled 0 degrees apart have dot of 1, 45 degrees is around 0.707 (half of the square root of 2), 90 degrees is 0,\nand when you go beyond that it goes negative the same way (145 degrees: dot = -0.707, 180 degrees: dot = -1).\n\ndot_right takes the viewer's eye angles and the root owner's RIGHT component of eye angles;\nmakes the dot product and compares it with the number defined in normal.\nfor example, dot_right below 0.707 should make something visible if you don't look beyond 45 degrees of the direction of the owner's side",
+
arguments = {{normal = "number"}},
callback = function(self, ent, normal)
@@ -1144,6 +2238,9 @@ PART.OldEvents = {
},
flat_dot_forward = {
+ operator_type = "number", preferred_operator = "above",
+ tutorial_explanation = "the dot product is a mathematical operation on vectors (angles / arrows / directions).\n\nfor reference, vectors angled 0 degrees apart have dot of 1, 45 degrees is around 0.707 (half of the square root of 2), 90 degrees is 0,\nand when you go beyond that it goes negative the same way (145 degrees: dot = -0.707, 180 degrees: dot = -1).\n\ndot_forward takes the viewer's eye angles and the root owner's FORWARD component of eye angles;\nmakes the dot product and compares it with the number defined in normal.\nfor example, dot_forward below 0.707 should make something visible if you don't look beyond 45 degrees of the direction of the owner's forward eye angles.\nflat means it's projecting onto a 2D plane, so if you're looking down it won't make a difference",
+
arguments = {{normal = "number"}},
callback = function(self, ent, normal)
local owner = self:GetRootPart():GetOwner()
@@ -1163,6 +2260,9 @@ PART.OldEvents = {
},
flat_dot_right = {
+ operator_type = "number", preferred_operator = "above",
+ tutorial_explanation = "the dot product is a mathematical operation on vectors (angles / arrows / directions).\n\nfor reference, vectors angled 0 degrees apart have dot of 1, 45 degrees is around 0.707 (half of the square root of 2), 90 degrees is 0,\nand when you go beyond that it goes negative the same way (145 degrees: dot = -0.707, 180 degrees: dot = -1).\n\ndot_right takes the viewer's eye angles and the root owner's RIGHT component of eye angles;\nmakes the dot product and compares it with the number defined in normal.\nfor example, dot_right below 0.707 should make something visible if you don't look beyond 45 degrees of the direction of the owner's side.\nflat means it's projecting onto a 2D plane, so if you're looking down it won't make a difference",
+
arguments = {{normal = "number"}},
callback = function(self, ent, normal)
local owner = self:GetRootPart():GetOwner()
@@ -1179,10 +2279,658 @@ PART.OldEvents = {
return 0
end
+ },
+
+ is_sitting = {
+ operator_type = "none",
+ callback = function(self, ent)
+ if not ent:IsPlayer() then return false end
+ local vehicle = ent:GetVehicle()
+ if ent.GetSitting then return ent:GetSitting() end --sit anywhere script
+ return IsValid(vehicle) and vehicle:GetModel() ~= "models/vehicles/prisoner_pod_inner.mdl" --no prison pod!
+ end
+ },
+
+ is_driving = {
+ operator_type = "none",
+ callback = function(self, ent)
+ if not ent:IsPlayer() then return false end
+ local vehicle = ent:GetVehicle()
+
+ if IsValid(vehicle) then --vehicle entity exists
+ if IsValid(vehicle:GetParent()) then --some vehicle seats have a parent
+ if vehicle:GetParent():GetClass() == "gmod_sent_vehicle_fphysics_base" and ent.IsDrivingSimfphys then --try simfphys
+ return ent:IsDrivingSimfphys() and ent:GetVehicle() == ent:GetSimfphys():GetDriverSeat() --in simfphys vehicle and seat is the driver seat
+ elseif vehicle:GetParent().BaseClass and vehicle:GetParent().BaseClass.ClassName == "wac_hc_base" then --try with WAC aircraft too
+ return vehicle == vehicle.wac_seatswitcher.seats[1] --first seat
+ end
+ elseif vehicle:GetClass() == "prop_vehicle_prisoner_pod" then --we don't want bare seats or prisoner pod
+ if vehicle.HandleAnimation == true and not isfunction(vehicle.HandleAnimation) and vehicle:GetModel() ~= "models/vehicles/prisoner_pod_inner.mdl" then --exclude prisoner pod and narrow down to SCars
+ return true
+ end
+ return false
+ else --assume that most other classes than prop_vehicle_prisoner_pod are drivable vehicles
+ return true
+ end
+ end
+ return false
+ end
+ },
+
+ is_passenger = {
+ operator_type = "none",
+ callback = function(self, ent)
+ if not ent:IsPlayer() then return false end
+ local vehicle = ent:GetVehicle()
+
+ if IsValid(vehicle) then --vehicle entity exists
+ if IsValid(vehicle:GetParent()) then --some vehicle seats have a parent
+ if vehicle:GetParent():GetClass() == "gmod_sent_vehicle_fphysics_base" and ent.IsDrivingSimfphys then --try simfphys
+ return ent:IsDrivingSimfphys() and ent:GetVehicle() ~= ent:GetSimfphys():GetDriverSeat() --in simfphys vehicle and seat is the driver seat
+ elseif vehicle:GetParent().BaseClass and vehicle:GetParent().BaseClass.ClassName == "wac_hc_base" then --try with WAC aircraft too
+ return vehicle ~= vehicle.wac_seatswitcher.seats[1] --first seat
+ end
+ elseif vehicle:GetClass() == "prop_vehicle_prisoner_pod" then --we can count bare seats and prisoner pods as passengers
+ return true
+ else --assume that most other classes than prop_vehicle_prisoner_pod are drivable vehicles, but they're also probably single seaters so...
+ return false
+ end
+ end
+ return false
+ end
+ },
+
+ weapon_iron_sight = {
+ operator_type = "none",
+ callback = function(self, ent)
+ if not IsValid(ent) or ent:Health() < 1 then return false end
+ if not ent.GetActiveWeapon then return false end
+ if not IsValid(ent:GetActiveWeapon()) then return false end
+ local wep = ent:GetActiveWeapon()
+ if wep.IsFAS2Weapon then
+ return wep.dt.Status == FAS_STAT_ADS
+ end
+
+ if wep.GetIronSights then return wep:GetIronSights() end
+ if wep.Sighted then return wep:GetActiveSights() end --arccw
+ return false
+ end
+ },
+
+ weapon_firemode = {
+ operator_type = "mixed",
+ arguments = {{name_or_id = "string"}},
+ callback = function(self, ent, name_or_id)
+ name_or_id = string.lower(name_or_id)
+ if not IsValid(ent) or ent:Health() < 1 then return false end
+ if not ent.GetActiveWeapon then return false end
+ if not IsValid(ent:GetActiveWeapon()) then return false end
+ local wep = ent:GetActiveWeapon()
+
+ if wep.ArcCW then
+ if wep.Firemodes[wep:GetFireMode()] then --some use a Firemodes table
+ if wep.Firemodes[wep:GetFireMode()].PrintName then
+ return
+ self:StringOperator(name_or_id, wep.Firemodes[wep:GetFireMode()].PrintName)
+ or self:StringOperator(name_or_id, wep:GetFiremodeName())
+ or self:NumberOperator(wep:GetFireMode(), tonumber(name_or_id))
+ end
+ elseif wep.Primary then
+ if wep.Primary.Automatic ~= nil then
+ if wep.Primary.Automatic == true then
+ return name_or_id == "automatic" or name_or_id == "auto"
+ else
+ return name_or_id == "semi-automatic" or name_or_id == "semi-auto" or name_or_id == "single"
+ end
+ end
+ self:StringOperator(name_or_id, wep:GetFiremodeName())
+ end
+ return self:StringOperator(name_or_id, wep:GetFiremodeName()) or self:NumberOperator(wep:GetFireMode(), tonumber(name_or_id))
+ end
+
+ if wep.IsFAS2Weapon then
+ if not wep.FireMode then return name_or_id == "" or name_or_id == "nil" or name_or_id == "null" or name_or_id == "none"
+ else return self:StringOperator(wep.FireMode, name_or_id) end
+ end
+
+ if wep.GetFireModeName then --TFA base is an arbitrary number and name (language-specific)
+ return self:StringOperator(string.lower(wep:GetFireModeName()), name_or_id) or self:NumberOperator(wep:GetFireMode(), tonumber(name_or_id))
+ end
+
+ if wep.Primary then
+ if wep.Primary.Automatic ~= nil then --M9K is a boolean
+ if wep.Primary.Automatic == true then
+ return name_or_id == "automatic" or name_or_id == "auto" or name_or_id == "1"
+ else
+ return name_or_id == "semi-automatic" or name_or_id == "semi-auto" or name_or_id == "single" or name_or_id == "0"
+ end
+ end
+ end
+
+
+ return false
+ end,
+ nice = function(self, ent, name_or_id)
+ if not IsValid(ent) then return end
+ if not ent.GetActiveWeapon then return false end
+ if not IsValid(ent:GetActiveWeapon()) then return "invalid weapon" end
+ wep = ent:GetActiveWeapon()
+ local str = "weapon_firemode ["..self.Operator.. " " .. name_or_id .. "] | "
+
+ if wep.IsFAS2Weapon then
+
+ if wep.FireMode then
+ str = str .. wep.FireMode .. " | options : "
+ for i,v in ipairs(wep.FireModes) do
+ str = str .. "(" .. v .. " = " .. i.. "), "
+ end
+ else str = str .. "" end
+ return str
+ end
+
+ if wep.ArcCW then
+ if not IsValid(wep) then return "no active weapon" end
+ if wep.GetFiremodeName then
+ str = str .. wep:GetFiremodeName() .. " | options : "
+ for i,v in ipairs(wep.Firemodes) do
+ if v.PrintName then
+ str = str .. "(" .. i .. " = " .. v.PrintName .. "), "
+ end
+ end
+ if wep.Primary.Automatic then
+ str = str .. "(" .. "Automatic" .. "), "
+ end
+ end
+ return str
+ end
+
+ if wep.GetFireModeName then --TFA base or arccw
+ if not IsValid(wep) then return "no active weapon" end
+ if wep.GetFireModeName then
+ str = str .. wep:GetFireModeName() .. " | options : "
+ for i,v in ipairs(wep:GetStatL("FireModes")) do
+ str = str .. "(" .. v .. " = " .. i.. "), "
+ end
+ end
+ return str
+ end
+
+ if wep.Primary then --M9K
+ if wep.Primary.Automatic ~= nil then
+ if wep.Primary.Automatic then
+ str = str .. "automatic"
+ else
+ str = str .. "semi-auto"
+ end
+ end
+ str = str .. " | options : 1/auto/automatic, 0/single/semi-auto/semi-automatic"
+ return str
+ end
+
+
+ return str
+ end
+ },
+
+ weapon_safety = {
+ operator_type = "none",
+ callback = function(self, ent)
+ if not ent or not IsValid(ent) then return false end
+ if not ent.GetActiveWeapon then return false end
+ if not IsValid(ent:GetActiveWeapon()) then return false end
+ local wep = ent:GetActiveWeapon()
+ if wep.IsSafety then
+ return wep:IsSafety()
+ end
+ if wep.ArcCW then
+ return wep:GetFiremodeName() == "Safety"
+ end
+
+ return false
+ end
+ },
+
+ damage_zone_hit = {
+ operator_type = "number", preferred_operator = "above",
+ arguments = {{time = "number"}, {damage = "number"}, {uid = "string"}},
+ userdata = {{default = 1}, {default = 0}, {default = "", enums = function(part)
+ local output = {}
+ local parts = pac.GetLocalParts()
+
+ for i, part in pairs(parts) do
+ if part.ClassName == "damage_zone" then
+ output["[UID:" .. string.sub(i,1,16) .. "...] " .. part:GetName() .. "; in " .. part:GetParent().ClassName .. " " .. part:GetParent():GetName()] = part.UniqueID
+ end
+ end
+
+ return output
+ end}},
+ callback = function(self, ent, time, damage, uid)
+ uid = uid or ""
+ uid = string.gsub(uid, "\"", "")
+ if uid == "" then
+ --for _,part in pairs(pac.GetLocalParts()) do
+ for _,part in ipairs(self.specialtrackedparts) do
+ if part.ClassName == "damage_zone" then
+ if part.dmgzone_hit_done and self:NumberOperator(part.Damage, damage) then
+ if part.dmgzone_hit_done + time > CurTime() then
+ return true
+ end
+ end
+ end
+ end
+ else
+ local part = self:GetOrFindCachedPart(uid)
+ if not IsValid(part) then
+ self:SetError("invalid part Unique ID\n"..uid)
+ else
+ if part.ClassName == "damage_zone" then
+ if part.dmgzone_hit_done and self:NumberOperator(part.Damage, damage) then
+ if part.dmgzone_hit_done + time > CurTime() then
+ return true
+ end
+ end
+ self:SetError()
+ else
+ self:SetError("You set a UID that's not a damage zone!")
+ end
+ end
+ end
+ return false
+ end,
+ },
+
+ damage_zone_kill = {
+ operator_type = "mixed", preferred_operator = "above",
+ arguments = {{time = "number"}, {uid = "string"}},
+ userdata = {{default = 1}, {default = "", enums = function(part)
+ local output = {}
+ local parts = pac.GetLocalParts()
+
+ for i, part in pairs(parts) do
+ if part.ClassName == "damage_zone" then
+ output["[UID:" .. string.sub(i,1,16) .. "...] " .. part:GetName() .. "; in " .. part:GetParent().ClassName .. " " .. part:GetParent():GetName()] = part.UniqueID
+ end
+ end
+
+ return output
+ end}},
+ callback = function(self, ent, time, uid)
+ uid = uid or ""
+ uid = string.gsub(uid, "\"", "")
+ if uid == "" then
+ --for _,part in pairs(pac.GetLocalParts()) do
+ for _,part in ipairs(self.specialtrackedparts) do
+ if part.ClassName == "damage_zone" then
+ if part.dmgzone_kill_done then
+ if part.dmgzone_kill_done + time > CurTime() then
+ return true
+ end
+ end
+ end
+ end
+ else
+ local part = self:GetOrFindCachedPart(uid)
+ if not IsValid(part) then
+ self:SetError("invalid part Unique ID\n"..uid)
+ else
+ if part.ClassName == "damage_zone" then
+ if part.dmgzone_kill_done then
+ if part.dmgzone_kill_done + time > CurTime() then
+ return true
+ end
+ end
+ self:SetError()
+ else
+ self:SetError("You set a UID that's not a damage zone!")
+ end
+ end
+ end
+ return false
+ end,
+ },
+
+ lockpart_grabbed = {
+ operator_type = "none",
+ callback = function(self, ent)
+ return ent.IsGrabbed and ent.IsGrabbedByUID
+ end
+ },
+
+ lockpart_grabbing = {
+ operator_type = "none",
+ arguments = {{uid = "string"}},
+ userdata = {{enums = function(part)
+ local output = {}
+ local parts = pac.GetLocalParts()
+
+ for i, part in pairs(parts) do
+ if part.ClassName == "lock" then
+ output["[UID:" .. string.sub(i,1,16) .. "...] " .. part:GetName() .. "; in " .. part:GetParent().ClassName .. " " .. part:GetParent():GetName()] = part.UniqueID
+ end
+ end
+
+ return output
+ end}},
+ callback = function(self, ent, uid)
+ uid = uid or ""
+ uid = string.gsub(uid, "\"", "")
+ if uid == "" then
+ --for _,part in pairs(pac.GetLocalParts()) do
+ for _,part in ipairs(self.specialtrackedparts) do
+ if part.ClassName == "lock" then
+ if part.grabbing then
+ return IsValid(part.target_ent)
+ end
+ end
+ end
+ else
+ local part = self:GetOrFindCachedPart(uid)
+ if not IsValid(part) then
+ self:SetError("invalid part Unique ID\n"..uid)
+ else
+ if part.ClassName == "lock" then
+ if part.grabbing then
+ return IsValid(part.target_ent)
+ end
+ self:SetError()
+ else
+ self:SetError("You set a UID that's not a lock part!")
+ end
+ end
+ end
+ return false
+ end
+ },
+
+ --[[
+ ent.pac_healthbars_layertotals = ent.pac_healthbars_layertotals or {}
+ ent.pac_healthbars_uidtotals = ent.pac_healthbars_uidtotals or {}
+ ent.pac_healthbars_total = 0
+ ]]
+ healthmod_bar_total = {
+ operator_type = "number", preferred_operator = "above",
+ arguments = {{HpValue = "number"}},
+ userdata = {{default = 0}},
+ callback = function(self, ent, HpValue)
+ if ent.pac_healthbars and ent.pac_healthbars_total then
+ return self:NumberOperator(ent.pac_healthbars_total, HpValue)
+ end
+ return false
+ end,
+ nice = function(self, ent, HpValue)
+ local str = "healthmod_bar_total : [" .. self.Operator .. " " .. HpValue .. "]"
+ if ent.pac_healthbars_total then
+ str = str .. " | " .. ent.pac_healthbars_total
+ end
+ return str
+ end
+ },
+
+ healthmod_bar_layertotal = {
+ operator_type = "number", preferred_operator = "above",
+ arguments = {{HpValue = "number"}, {layer = "number"}},
+ userdata = {{default = 0}, {default = 0}},
+ callback = function(self, ent, HpValue, layer)
+ if ent.pac_healthbars and ent.pac_healthbars_layertotals then
+ if ent.pac_healthbars_layertotals[layer] then
+ return self:NumberOperator(ent.pac_healthbars_layertotals[layer], HpValue)
+ end
+
+ end
+ return false
+ end,
+ nice = function(self, ent, HpValue, layer)
+ local str = "healthmod_layer_total at layer " .. layer .. " : [" .. self.Operator .. " " .. HpValue .. "]"
+ if ent.pac_healthbars_layertotals then
+ if ent.pac_healthbars_layertotals[layer] then
+ str = str .. " | " .. ent.pac_healthbars_layertotals[layer]
+ else
+ str = str .. " | not found"
+ end
+
+ else
+ str = str .. " | not found"
+ end
+ return str
+ end
+ },
+
+ healthmod_bar_uidvalue = {
+ operator_type = "number", preferred_operator = "above",
+ arguments = {{HpValue = "number"}, {part_uid = "string"}},
+ userdata = {{default = 0}, {enums = function(part)
+ local output = {}
+ local parts = pac.GetLocalParts()
+
+ for i, part in pairs(parts) do
+ if part.ClassName == "health_modifier" then
+ output["[UID:" .. string.sub(i,1,16) .. "...] " .. part:GetName() .. "; in " .. part:GetParent().ClassName .. " " .. part:GetParent():GetName()] = part.UniqueID
+ end
+ end
+
+ return output
+ end}},
+ callback = function(self, ent, HpValue, part_uid)
+ part_uid = part_uid or ""
+ part_uid = string.gsub(part_uid, "\"", "")
+ if ent.pac_healthbars and ent.pac_healthbars_uidtotals then
+ if ent.pac_healthbars_uidtotals[part_uid] then
+ return self:NumberOperator(ent.pac_healthbars_uidtotals[part_uid], HpValue)
+ end
+ end
+ return false
+ end,
+ nice = function(self, ent, HpValue, part_uid)
+ local str = "healthmod_bar_uidvalue : [" .. self.Operator .. " " .. HpValue .. "]"
+ if ent.pac_healthbars_uidtotals then
+ if ent.pac_healthbars_uidtotals[part_uid] then
+ str = str .. " | " .. ent.pac_healthbars_uidtotals[part_uid]
+ else
+ str = str .. " | nothing for UID "..part_uid
+ end
+ else
+ str = str .. " | nothing for UID "..part_uid
+ end
+ return str
+ end
+ },
+
+ healthmod_bar_hit = {
+ operator_type = "number", preferred_operator = "above",
+ arguments = {{amount = "number"}, {layer = "string"}, {uid = "string"}, {time = "number"}},
+ userdata = {{default = 0}, {default = ""}, {default = ""}, {default = 1}},
+ callback = function(self, ent, amount, layer, uid, time)
+ if not ent.pac_healthbars or not ent.pac_healthbars_total_updated then return false end
+ local check_layer = layer ~= ""
+ layer = tonumber(layer)
+ local check_uid = uid ~= ""
+ --[[
+ ent.pac_healthbars_total_updated = {time = CurTime(), delta = 0}
+ ent.pac_healthbars_layers_updated = {time = CurTime(), deltas = {}}
+ ent.pac_healthbars_uids_updated = {time = CurTime(), deltas = {}}
+ ]]
+ if not check_layer and not check_uid then
+ if ent.pac_healthbars_total_updated.time + time > CurTime() then
+ return self:NumberOperator(ent.pac_healthbars_total_updated.delta, amount)
+ end
+ elseif check_layer and (layer ~= nil) and ent.pac_healthbars_layers_updated.deltas[layer] then
+ if ent.pac_healthbars_layers_updated.time + time > CurTime() then
+ return self:NumberOperator(ent.pac_healthbars_layers_updated.deltas[layer], amount)
+ end
+ elseif check_uid and ent.pac_healthbars_uids_updated.deltas[uid] then
+ if ent.pac_healthbars_uids_updated.time + time > CurTime() then
+ return self:NumberOperator(ent.pac_healthbars_uids_updated.deltas[uid], amount)
+ end
+ end
+ return false
+ end
+ },
+
+ or_gate = {
+ operator_type = "none", preferred_operator = "find simple",
+ tutorial_explanation = "combines multiple events into an OR gate, the event will activate as soon as one of the events listed is activated (taking inverts into account).\n\nuids is a list (separated by semicolons) of part identifiers (UniqueIDs or names)\n\nAn easy way to gather them is to use bulk select (ctrl+click) and to right click back on the or_gate",
+ arguments = {{uids = "string"}, {ignore_inverts = "boolean"}},
+ userdata = {{default = "", enums = function(part)
+ local output = {}
+ local parts = pac.GetLocalParts()
+ for i, part in pairs(parts) do
+ if part.ClassName == "event" then
+ output["[UID:" .. string.sub(i,1,16) .. "...] " .. part:GetName() .. "; in " .. part:GetParent().ClassName .. " " .. part:GetParent():GetName()] = part.UniqueID
+ end
+ end
+ return output
+ end}},
+ callback = function(self, ent, uids, ignore_inverts)
+ if uids == "" then return false end
+ local uid_splits = string.Split(uids, ";")
+ local true_count = 0
+ for i,uid in ipairs(uid_splits) do
+ local part = self:GetOrFindCachedPart(uid)
+ if part:IsValid() then
+ local raw = part.raw_event_condition
+ local b = false
+ if ignore_inverts then
+ b = raw
+ else
+ b = not part.event_triggered
+ end
+ if b then
+ true_count = true_count + 1
+ end
+ end
+ end
+ return true_count > 0
+ end,
+ },
+ xor_gate = {
+ operator_type = "none", preferred_operator = "find simple",
+ tutorial_explanation = "combines multiple events into an XOR gate, the event will activate if one (and only one) of the two events is activated (taking inverts into account).\n\nuid1 and uid2 are part identifiers (UniqueID or names) for events",
+ arguments = {{uid1 = "string"},{uid2 = "string"}},
+ userdata = {
+ {default = "", enums = function(part)
+ local output = {}
+ local parts = pac.GetLocalParts()
+ for i, part in pairs(parts) do
+ if part.ClassName == "event" then
+ output["[UID:" .. string.sub(i,1,16) .. "...] " .. part:GetName() .. "; in " .. part:GetParent().ClassName .. " " .. part:GetParent():GetName()] = part.UniqueID
+ end
+ end
+ return output
+ end},
+ {default = "", enums = function(part)
+ local output = {}
+ local parts = pac.GetLocalParts()
+ for i, part in pairs(parts) do
+ if part.ClassName == "event" then
+ output["[UID:" .. string.sub(i,1,16) .. "...] " .. part:GetName() .. "; in " .. part:GetParent().ClassName .. " " .. part:GetParent():GetName()] = part.UniqueID
+ end
+ end
+ return output
+ end}
+ },
+ callback = function(self, ent, uid1, uid2)
+ if uid1 == "" then return false end
+ if uid2 == "" then return false end
+ local part1 = self:GetOrFindCachedPart(uid1)
+ local part2 = self:GetOrFindCachedPart(uid2)
+ if not IsValid(part1) or not IsValid(part2) then return false end
+ local b1 = part1.event_triggered if part1.Invert then b1 = not b1 end
+ local b2 = part2.event_triggered if part2.Invert then b2 = not b2 end
+ return ((b1 and not b2) or (b2 and not b1)) and not (b1 and b2)
+ end,
+ nice = function(self, ent, uid1, uid2)
+ local part1 = self:GetOrFindCachedPart(uid1)
+ local part2 = self:GetOrFindCachedPart(uid2)
+ if not IsValid(part1) or not IsValid(part2) then return "xor_gate : [" .. uid1 .. ", " .. uid2 .. "] " end
+ local str = "xor_gate : [" .. part1:GetName() .. ", " .. part2:GetName() .. "]"
+ return str
+ end
+ },
+ and_gate = {
+ operator_type = "none", preferred_operator = "find simple",
+ tutorial_explanation = "combines multiple events into an AND gate, the event will activate when all the events listed are activated (taking inverts into account)\n\nuids is a list (separated by semicolons) of part identifiers (UniqueIDs or names)\n\nAn easy way to gather them is to use bulk select (ctrl+click) and to right click back on the and_gate",
+ arguments = {{uids = "string"}, {ignore_inverts = "boolean"}},
+ userdata = {{default = "", enums = function(part)
+ local output = {}
+ local parts = pac.GetLocalParts()
+ for i, part in pairs(parts) do
+ if part.ClassName == "event" then
+ output["[UID:" .. string.sub(i,1,16) .. "...] " .. part:GetName() .. "; in " .. part:GetParent().ClassName .. " " .. part:GetParent():GetName()] = part.UniqueID
+ end
+ end
+ return output
+ end}},
+ callback = function(self, ent, uids, ignore_inverts)
+ if uids == "" then return false end
+ local uid_splits = string.Split(uids, ";")
+ for i,uid in ipairs(uid_splits) do
+ local part = self:GetOrFindCachedPart(uid)
+ if part:IsValid() then
+ local raw = part.raw_event_condition
+ local b = false
+ if ignore_inverts then
+ b = raw
+ else
+ b = not part.event_triggered
+ end
+
+ if not b then
+ return false
+ end
+ else
+ return false
+ end
+ end
+ return true
+ end,
}
+
}
+
do
+
+ --[[local base_input_enums_names = {
+ ["IN_ATTACK"] = 1,
+ ["IN_JUMP"] = 2,
+ ["IN_DUCK"] = 4,
+ ["IN_FORWARD"] = 8,
+ ["IN_BACK"] = 16,
+ ["IN_USE"] = 32,
+ ["IN_CANCEL"] = 64,
+ ["IN_LEFT"] = 128,
+ ["IN_RIGHT"] = 256,
+ ["IN_MOVELEFT"] = 512,
+ ["IN_MOVERIGHT"] = 1024,
+ ["IN_ATTACK2"] = 2048,
+ ["IN_RUN"] = 4096,
+ ["IN_RELOAD"] = 8192,
+ ["IN_ALT1"] = 16384,
+ ["IN_ALT2"] = 32768,
+ ["IN_SCORE"] = 65536,
+ ["IN_SPEED"] = 131072,
+ ["IN_WALK"] = 262144,
+ ["IN_ZOOM"] = 524288,
+ ["IN_WEAPON1"] = 1048576,
+ ["IN_WEAPON2"] = 2097152,
+ ["IN_BULLRUSH"] = 4194304,
+ ["IN_GRENADE1"] = 8388608,
+ ["IN_GRENADE2"] = 16777216
+ }
+ local input_aliases = {}
+
+ for name,value in pairs(base_input_enums_names) do
+ local alternative0 = string.lower(name)
+ local alternative1 = string.Replace(string.lower(name),"in_","")
+ local alternative2 = "+"..alternative1
+ input_aliases[name] = value
+ input_aliases[alternative0] = value
+ input_aliases[alternative1] = value
+ input_aliases[alternative2] = value
+ end]]
+
+
local enums = {}
local enums2 = {}
for key, val in pairs(_G) do
@@ -1198,6 +2946,9 @@ do
end
pac.key_enums = enums
+ pac.key_enums_reverse = enums2
+
+--@note button broadcast
--TODO: Rate limit!!!
net.Receive("pac.BroadcastPlayerButton", function()
@@ -1210,17 +2961,47 @@ do
local key = net.ReadUInt(8)
local down = net.ReadBool()
+ if not pac.key_enums then --rebuild the enums
+ local enums = {}
+ local enums2 = {}
+ for key, val in pairs(_G) do
+ if isstring(key) and isnumber(val) then
+ if key:sub(0,4) == "KEY_" and not key:find("_LAST$") and not key:find("_FIRST$") and not key:find("_COUNT$") then
+ enums[val] = key:sub(5):lower()
+ enums2[enums[val]] = val
+ elseif (key:sub(0,6) == "MOUSE_" or key:sub(0,9) == "JOYSTICK_") and not key:find("_LAST$") and not key:find("_FIRST$") and not key:find("_COUNT$") then
+ enums[val] = key:lower()
+ enums2[enums[val]] = val
+ end
+ end
+ end
+ pac.key_enums = enums
+ pac.key_enums_reverse = enums2
+ end
+
key = pac.key_enums[key] or key
ply.pac_buttons = ply.pac_buttons or {}
ply.pac_buttons[key] = down
+
+
+ ply.pac_broadcasted_buttons_lastpressed = ply.pac_broadcasted_buttons_lastpressed or {}
+ if down then
+ ply.pac_broadcasted_buttons_lastpressed[key] = SysTime()
+ end
+
+ --outsource the part pool operations
+ pac.UpdateButtonEvents(ply, key, down)
+
+
end)
PART.OldEvents.button = {
- arguments = {{button = "string"}},
+ operator_type = "none",
+ arguments = {{button = "string"}, {holdtime = "number"}, {toggle = "boolean"}, {ignore_if_hidden = "boolean"}},
userdata = {{enums = function()
return enums
- end}},
+ end, default = "mouse_left"}, {default = 0}, {default = false}},
nice = function(self, ent, button)
local ply = self:GetPlayerOwner()
@@ -1240,8 +3021,13 @@ do
return self:GetOperator() .. " \"" .. button .. "\"" .. " in (" .. active .. ")"
end,
- callback = function(self, ent, button)
+ callback = function(self, ent, button, holdtime, toggle, ignore_if_hidden)
+ self.holdtime = holdtime or 0
+ local toggle = toggle or false
+ self.togglestate = self.togglestate or false
+
local ply = self:GetPlayerOwner()
+ self.pac_broadcasted_buttons_holduntil = self.pac_broadcasted_buttons_holduntil or {}
if ply == pac.LocalPlayer then
ply.pac_broadcast_buttons = ply.pac_broadcast_buttons or {}
@@ -1258,11 +3044,21 @@ do
local buttons = ply.pac_buttons
+ self.pac_broadcasted_buttons_holduntil[button] = self.pac_broadcasted_buttons_holduntil[button] or SysTime()
+
if buttons then
- return buttons[button]
+ if toggle then
+ return self.togglestate
+ elseif self.holdtime > 0 then
+ return SysTime() < self.pac_broadcasted_buttons_holduntil[button]
+ else
+ return buttons[button]
+ end
+
end
end,
}
+
end
do
@@ -1427,6 +3223,15 @@ do
eventObject.extra_nice_name = data.nice
+ local operator_type = data.operator_type
+ local preferred_operator = data.preferred_operator
+ local tutorial_explanation = data.tutorial_explanation
+ eventObject.operator_type = operator_type
+ eventObject.preferred_operator = preferred_operator
+ eventObject.tutorial_explanation = tutorial_explanation
+
+ PART.Tutorials[classname] = tutorial_explanation
+
function eventObject:Think(event, ent, ...)
return think(event, ent, ...)
end
@@ -1436,6 +3241,7 @@ do
timer.Simple(0, function() -- After all addons has loaded
hook.Call('PAC3RegisterEvents', nil, pac.CreateEvent, pac.RegisterEvent)
+ pace.TUTORIALS["events"] = PART.Tutorials
end)
end
@@ -1443,6 +3249,8 @@ end
do
local animations = pac.animations
local event = {
+ operator_type = "none",
+ tutorial_explanation = "selecting a custom animation part via UID,\nthis event activates whenever the linked custom animation is currently playing somewhere between the frames specified",
name = "custom_animation_frame",
nice = function(self, ent, animation)
if animation == "" then self:SetWarning("no animation selected") return "no animation" end
@@ -1452,20 +3260,7 @@ do
return part:GetName()
end,
args = {
- {"animation", "string", {
- enums = function(part)
- local output = {}
- local parts = pac.GetLocalParts()
-
- for i, part in pairs(parts) do
- if part.ClassName == "custom_animation" then
- output[i] = part
- end
- end
-
- return output
- end
- }},
+ {"animation", "string", {editor_panel = "custom_animation_frame"}},
{"frame_start", "number", {
editor_onchange = function(self, num)
local anim = pace.current_part:GetProperty("animation")
@@ -1504,6 +3299,7 @@ do
if v == ent then
local part = pac.GetPartFromUniqueID(pac.Hash(ent), animation)
if not IsValid(part) then return end
+ if part.ClassName ~= "custom_animation" then return end
local frame, delta = animations.GetEntityAnimationFrame(ent, part:GetAnimID())
if not frame or not delta then return end -- different animation part is playing
return frame >= frame_start and frame <= frame_end
@@ -1517,6 +3313,15 @@ do
eventObject.IsAvailable = event.available
eventObject.extra_nice_name = event.nice
+ data = event
+
+ local operator_type = data.operator_type
+ local preferred_operator = data.preferred_operator
+ local tutorial_explanation = data.tutorial_explanation
+ eventObject.operator_type = operator_type
+ eventObject.preferred_operator = preferred_operator
+ eventObject.tutorial_explanation = tutorial_explanation
+
pac.RegisterEvent(eventObject)
end
@@ -1617,6 +3422,13 @@ do
return isDarkRP() and available()
end
+ local operator_type = v.operator_type
+ local preferred_operator = v.preferred_operator
+ local tutorial_explanation = v.tutorial_explanation
+ eventObject.operator_type = operator_type
+ eventObject.preferred_operator = preferred_operator
+ eventObject.tutorial_explanation = tutorial_explanation
+
pac.RegisterEvent(eventObject)
end
end
@@ -1631,12 +3443,27 @@ function PART:GetParentEx()
return self:GetParent()
end
+function PART:GetTargetingModePrefix()
+ local modes = {}
+ if self.AffectChildrenOnly then
+ table.insert(modes, "ACO")
+ end
+ if IsValid(self:GetDestinationPart()) then
+ table.insert(modes, "TP")
+ end
+ if self.MultiTargetPart then
+ table.insert(modes, "MTP")
+ end
+ if table.IsEmpty(modes) then return "" end
+ return "[" .. table.concat(modes, " ") .. "] "
+end
+
function PART:GetNiceName()
local event_name = self:GetEvent()
if not PART.Events[event_name] then return "unknown event" end
- return PART.Events[event_name]:GetNiceName(self, get_owner(self))
+ return self:GetTargetingModePrefix() .. PART.Events[event_name]:GetNiceName(self, get_owner(self))
end
local function is_hidden_by_something_else(part, ignored_part)
@@ -1674,6 +3501,7 @@ local function should_trigger(self, ent, eventObject)
else
b = eventObject:Think(self, ent, self:GetParsedArgumentsForObject(eventObject)) or false
end
+ self.raw_event_condition = b
if self.Invert then
b = not b
@@ -1690,13 +3518,50 @@ end
PART.last_event_triggered = false
+function PART:fix_args()
+ local args = string.Split(self.Arguments, "@@")
+ if self.Events[self.Event] then
+ if self.Events[self.Event].__registeredArguments then
+ --PrintTable(self.Events[self.Event].__registeredArguments)
+ if #self.Events[self.Event].__registeredArguments ~= #args then
+ for argn,arg in ipairs(self.Events[self.Event].__registeredArguments) do
+ if not args[argn] or args[argn] == "" then
+ local added_arg = "0"
+ if arg[2] == "boolean" then
+ if arg[3] then
+ if arg[3].default then added_arg = "1"
+ else added_arg = "0" end
+ end
+ else
+ if arg[3] then
+ if arg[3].default then
+ added_arg = tostring(arg[3].default)
+ end
+ end
+ end
+ args[argn] = added_arg
+ end
+ end
+ self.Arguments = table.concat(args, "@@")
+ end
+ end
+ end
+end
+
function PART:OnThink()
+ self.nextactivationrefresh = self.nextactivationrefresh or CurTime()
+ if not self.singleactivatestate and self.nextactivationrefresh < CurTime() then
+ self.singleactivatestate = true
+ end
+
local ent = get_owner(self)
if not ent:IsValid() then return end
local data = PART.Events[self.Event]
+
if not data then return end
+ self:fix_args()
self:TriggerEvent(should_trigger(self, ent, data))
if pace and pace.IsActive() and self.Name == "" then
@@ -1707,17 +3572,97 @@ function PART:OnThink()
end
+function PART:SetAffectChildrenOnly(b)
+ if b == nil then return end
+
+ if self.AffectChildrenOnly ~= nil and self.AffectChildrenOnly ~= b then
+ --print("changing")
+ local ent = get_owner(self)
+ local data = PART.Events[self.Event]
+
+ if ent:IsValid() and data then
+ local b = should_trigger(self, ent, data)
+ if self.AffectChildrenOnly then
+ local parent = self:GetParent()
+ if parent:IsValid() then
+ parent:SetEventTrigger(self, b)
+
+ for _, child in ipairs(self:GetChildren()) do
+ if child.active_events[self] then
+ child.active_events[self] = nil
+ child.active_events_ref_count = child.active_events_ref_count - 1
+ child:CallRecursive("CalcShowHide", false)
+ end
+ end
+ end
+
+ else
+ for _, child in ipairs(self:GetChildren()) do
+ child:SetEventTrigger(self, b)
+ end
+ if self:GetParent():IsValid() then
+ local parent = self:GetParent()
+ if parent.active_events[self] then
+ parent.active_events[self] = nil
+ parent.active_events_ref_count = parent.active_events_ref_count - 1
+ parent:CallRecursive("CalcShowHide", false)
+ end
+ end
+
+ end
+ end
+ end
+ self.AffectChildrenOnly = b
+end
+
+function PART:OnRemove()
+ if not self.AffectChildrenOnly then
+ local parent = self:GetParent()
+ if parent:IsValid() then
+ parent.active_events[self] = nil
+ parent.active_events_ref_count = parent.active_events_ref_count - 1
+ parent:CalcShowHide()
+ end
+ end
+ if IsValid(self.DestinationPart) then
+ self.DestinationPart.active_events[self] = nil
+ self.DestinationPart.active_events_ref_count = self.DestinationPart.active_events_ref_count - 1
+ self.DestinationPart:CalcShowHide()
+ end
+ pac.RegisterPartToCache(self:GetPlayerOwner(), "button_events", self, true)
+end
+
function PART:TriggerEvent(b)
self.event_triggered = b -- event_triggered is just used for the editor
+ local single_targetpart = IsValid(self.DestinationPart)
+
+ if single_targetpart then
+ self.DestinationPart:SetEventTrigger(self, b)
+ self.previousdestinationpart = self.DestinationPart
+ else
+ if IsValid(self.previousdestinationpart) then
+ if self.DestinationPart ~= self.previousdestinationpart then --when editing, if we change the destination part we need to reset the old one
+ self.previousdestinationpart:SetEventTrigger(self, false)
+ end
+ end
+ end
+
+ if self.MultiTargetPart then
+ for _,part2 in ipairs(self.MultiTargetPart) do
+ if part2.SetEventTrigger then part2:SetEventTrigger(self, b) end
+ end
+ end
if self.AffectChildrenOnly then
for _, child in ipairs(self:GetChildren()) do
child:SetEventTrigger(self, b)
end
else
- local parent = self:GetParent()
- if parent:IsValid() then
- parent:SetEventTrigger(self, b)
+ if not single_targetpart and not self.MultiTargetPart then --normal parent mode should only happen if nothing is set
+ local parent = self:GetParent()
+ if parent:IsValid() then
+ parent:SetEventTrigger(self, b)
+ end
end
end
end
@@ -1866,6 +3811,8 @@ function PART:NumberOperator(a, b)
end
end
+
+
function PART:OnHide()
if self.timerx_reset then
self.time = nil
@@ -1889,6 +3836,8 @@ function PART:OnShow()
self.time = nil
self.number = 0
end
+ self.showtime = CurTime()
+ self.singleactivatestate = true
end
function PART:OnAnimationEvent(ent)
@@ -1952,16 +3901,33 @@ pac.AddHook("EntityEmitSound", "emit_sound", function(data)
end
end)
-pac.AddHook("EntityFireBullets", "firebullets", function(ent, data)
- if not ent:IsValid() or not ent.pac_has_parts then return end
- ent.pac_fire_bullets = {name = data.AmmoType, time = pac.RealTime, reset = true}
+if game.SinglePlayer() then
+ net.Receive("pac_fire_bullets_for_singleplayer", function()
+ local ent = net.ReadEntity()
+ if not ent:IsValid() or not ent.pac_has_parts then return end
+ local ammo_type = net.ReadUInt(8)
+ ent.pac_fire_bullets = {name = game.GetAmmoName(ammo_type), time = pac.RealTime, reset = true}
+
+ pac.CallRecursiveOnAllParts("OnFireBullets")
+ end)
+else
+ pac.AddHook("EntityFireBullets", "firebullets", function(ent, data)
+ if not ent:IsValid() or not ent.pac_has_parts then return end
+ ent.pac_fire_bullets = {name = data.AmmoType, time = pac.RealTime, reset = true}
- pac.CallRecursiveOnAllParts("OnFireBullets")
+ pac.CallRecursiveOnAllParts("OnFireBullets")
- if ent.pac_hide_bullets then
- return false
- end
-end)
+ if ent.pac_hide_bullets then
+ return false
+ end
+ end)
+end
+
+--for regaining focus on cameras from first person, hacky thing to not loop through localparts every time
+--only if the received command name matches that of a camera's linked command event
+--we won't be finding from substrings
+pac.camera_linked_command_events = {}
+local initially_check_camera_linked_command_events = true
net.Receive("pac_event", function(umr)
local ply = net.ReadEntity()
@@ -1976,9 +3942,74 @@ net.Receive("pac_event", function(umr)
if ply:IsValid() then
ply.pac_command_events = ply.pac_command_events or {}
ply.pac_command_events[str] = {name = str, time = pac.RealTime, on = on}
+ if pac.LocalPlayer == ply then
+ if pac.camera_linked_command_events[str] then --if this might be related to a camera
+ pac.TryToAwakenDormantCameras()
+ elseif initially_check_camera_linked_command_events then --if it's not known, check only once for initialize this might be related to a camera
+ pac.TryToAwakenDormantCameras(true)
+ initially_check_camera_linked_command_events = false
+ end
+ end
+ end
+end)
+
+concommand.Add("pac_wipe_events", function(ply)
+ ply.pac_command_events = nil
+ ply.pac_command_event_sequencebases = nil
+ pac.camera_linked_command_events = {}
+end)
+concommand.Add("pac_print_events", function(ply)
+ ply.pac_command_events = ply.pac_command_events or {}
+ PrintTable(ply.pac_command_events)
+end)
+
+local sequence_verbosity = CreateConVar("pac_event_sequenced_verbosity", 1, FCVAR_ARCHIVE, "whether to print info when running pac_event_sequenced")
+net.Receive("pac_event_set_sequence", function(len)
+ local ply = net.ReadEntity()
+ local event = net.ReadString()
+ local num = net.ReadUInt(8)
+ ply.pac_command_events = ply.pac_command_events or {}
+ ply.pac_command_event_sequencebases = ply.pac_command_event_sequencebases or {}
+ ply.pac_command_event_sequencebases[event] = ply.pac_command_event_sequencebases[event] or {name = event}
+ data = ply.pac_command_event_sequencebases[event]
+ local previous_sequencenumber = data.current or 0
+
+ --assuming we don't know which parts of the series are active, nil out all of them before setting one
+ for i=0,100,1 do
+ ply.pac_command_events[event..i] = nil
+ end
+
+ if data.min then
+ if num < data.min then data.min = num end
+ else data.min = num end
+ if data.max then
+ if num > data.max then data.max = num end
+ else data.max = num end
+
+ if ply.pac_command_event_sequencebases then
+ if ply.pac_command_event_sequencebases[event] then
+ ply.pac_command_events[event..num] = {name = event..num, time = pac.RealTime, on = 1}
+ ply.pac_command_event_sequencebases[event].current = num
+ end
+ end
+ if sequence_verbosity:GetBool() and (ply == pac.LocalPlayer) then pac.Message("sequencing event series: " .. event .. "\n\t" .. previous_sequencenumber .. "->" .. num .. " / " .. data.max) end
+end)
+
+net.Receive("pac_event_update_sequence_bounds", function(len)
+ local ply = net.ReadEntity()
+ local tbl = net.ReadTable()
+ if not ply:IsPlayer() then return end
+ ply.pac_command_event_sequencebases = ply.pac_command_event_sequencebases or {}
+ for cmd, bounds in pairs(tbl) do
+ local current = 0
+ if ply.pac_command_event_sequencebases[cmd] then
+ current = ply.pac_command_event_sequencebases[cmd].current
+ end
+ ply.pac_command_event_sequencebases[cmd] = {min = bounds[1], max = bounds[2], current = current}
end
end)
+
pac.AddHook("OnPlayerChat", "say_event", function(ply, str)
if ply:IsValid() then
ply.pac_say_event = {str = str, time = pac.RealTime}
@@ -2032,39 +4063,223 @@ reload
custom gesture
--]]
+local eventwheel_visibility_rule = CreateConVar("pac_eventwheel_visibility_rule" , "0", FCVAR_ARCHIVE,
+"Different ways to filter your command events for the wheel.\n"..
+"-1 ignores hide flags completely\n"..
+"0 will hide a command if at least one event of one name has the \"hide in event wheel\" flag\n"..
+"1 will hide a command only if ALL events of one name have the \"hide in event wheel\" flag\n"..
+"2 will hide a command as soon as one event of a name is being hidden\n"..
+"3 will hide a command only if ALL events of a name are being hidden\n"..
+"4 will only show commands containing the following substrings, separated by spaces\n"..
+"-4 will hide commands containing the following substrings, separated by spaces")
+
+local eventwheel_style = CreateConVar("pac_eventwheel_style", "0", FCVAR_ARCHIVE, "The style of the eventwheel.\n0 is the default legacy style with one circle\n1 is the new style with colors, using one circle for the color and one circle for the activation indicator\n2 is an alternative style using a smaller indicator circle on the corner of the circle")
+local eventlist_style = CreateConVar("pac_eventlist_style", "0", FCVAR_ARCHIVE, "The style of the eventwheel list alternative.\n0 is like the default eventwheel legacy style with one indicator for the activation\n1 is the new style with colors, using one rectangle for the color and one rectangle for the activation indicator\n2 is an alternative style using a smaller indicator on the corner")
+local show_customize_button = CreateConVar("pac_eventwheel_show_customize_button", "1", FCVAR_ARCHIVE, "Whether to show the Customize button with the event wheel.")
+
+local eventwheel_font = CreateConVar("pac_eventwheel_font", "DermaDefault", FCVAR_ARCHIVE, "pac3 eventwheel font. try pac_font_ such as pac_font_20 or pac_font_bold30. the pac fonts go up to 34")
+local eventwheel_clickable = CreateConVar("pac_eventwheel_clickmode", "0", FCVAR_ARCHIVE, "The activation modes for pac3 event wheel.\n-1 : not clickable, but activate on menu close\n0 : clickable, and activate on menu close\n1 : clickable, but doesn't activate on menu close")
+local eventlist_clickable = CreateConVar("pac_eventlist_clickmode", "0", FCVAR_ARCHIVE, "The activation modes for pac3 event wheel list alternative.\n-1 : not clickable, but activate a hovered event on menu close\n0 : clickable, and activate a hovered event on menu close\n1 : clickable, but doesn't do anything on menu close")
+
+local event_list_font = CreateConVar("pac_eventlist_font", "DermaDefault", FCVAR_ARCHIVE, "The font for the eventwheel's rectangle list counterpart. It will also scale the rectangles' height.\nMight not work if the font is missing")
+
-- Custom event selector wheel
do
+
local function get_events()
+ pace.command_colors = pace.command_colors or {}
local available = {}
+ local names = {}
+ local args = string.Split(eventwheel_visibility_rule:GetString(), " ")
+ local uncolored_events = {}
for k,v in pairs(pac.GetLocalParts()) do
if v.ClassName == "event" then
local e = v:GetEvent()
if e == "command" then
local cmd, time, hide = v:GetParsedArgumentsForObject(v.Events.command)
- if hide then continue end
+ local this_event_hidden = v:IsHiddenBySomethingElse(false)
+
+
+ if not names[cmd] then
+ --wheel_hidden is the hide_in_eventwheel box
+ --possible_hidden is part hidden
+ names[cmd] = {
+ name = cmd, event = v,
+
+ wheel_hidden = hide,
+ all_wheel_hidden = hide,
+
+ possible_hidden = this_event_hidden,
+ all_possible_hidden = this_event_hidden,
+ }
+ else
+ --if already exists, we need to check counter examples for whether all members are hidden or hide_in_eventwheel
+
+ if not hide then
+ names[cmd].all_wheel_hidden = false
+ end
+
+ if not this_event_hidden then
+ names[cmd].all_possible_hidden = false
+ end
+
+ if not names[cmd].wheel_hidden and hide then
+ names[cmd].wheel_hidden = true
+ end
+
+ if not names[cmd].possible_hidden and this_event_hidden then
+ names[cmd].possible_hidden = true
+ end
+
+
+ end
- available[cmd] = {type = e, time = time}
+ available[cmd] = {type = e, time = time, trigger = cmd}
end
end
end
+ for cmd,v in pairs(names) do
+ uncolored_events[cmd] = not pace.command_colors[cmd]
+ local remove = false
+
+ if args[1] == "-1" then --skip
+ remove = false
+ elseif args[1] == "0" then --one hide_in_eventwheel
+ if v.wheel_hidden then
+ remove = true
+ end
+ elseif args[1] == "1" then --all hide_in_eventwheel
+ if v.all_wheel_hidden then
+ remove = true
+ end
+ elseif args[1] == "2" then --one hidden
+ if v.possible_hidden then
+ remove = true
+ end
+ elseif args[1] == "3" then --all hidden
+ if v.all_possible_hidden then
+ remove = true
+ end
+ elseif args[2] then
+ if #args > 1 then --args contains many strings
+ local match = false
+
+ for i=2, #args, 1 do
+ local str = args[i]
+ if string.find(cmd, str) then
+ match = true
+ end
+ end
+
+ if args[1] == "4" and not match then
+ remove = true
+ elseif args[1] == "-4" and match then
+ remove = true
+ end
+
+ else --why would you use the 4 or -4 mode if you didn't set keywords??
+ remove = false
+ end
+ end
+
+ if remove then
+ available[cmd] = nil
+ end
+ end
local list = {}
+
+
+ local colors = {}
+
+ for name,colstr in pairs(pace.command_colors) do
+ colors[colstr] = colors[colstr] or {}
+ colors[colstr][name] = available[name]
+ end
+
+
+ for col,tbl in pairs(colors) do
+
+ local sublist = {}
+ for k,v in pairs(tbl) do
+ table.insert(sublist,available[k])
+ end
+
+ table.sort(sublist, function(a, b) return a.trigger < b.trigger end)
+
+ for i,v in pairs(sublist) do
+ table.insert(list,v)
+ end
+ end
+
+ local uncolored_sublist = {}
+
for k,v in pairs(available) do
- v.trigger = k
+ if uncolored_events[k] then
+ table.insert(uncolored_sublist,available[k])
+ end
+ end
+
+ table.sort(uncolored_sublist, function(a, b) return a.trigger < b.trigger end)
+
+ for k,v in ipairs(uncolored_sublist) do
table.insert(list, v)
end
- table.sort(list, function(a, b) return a.trigger > b.trigger end)
+ --[[legacy behavior
+
+ for k,v in pairs(available) do
+ if k == names[k].name then
+ v.trigger = k
+ table.insert(list, v)
+ end
+ end
+
+ table.sort(list, function(a, b) return a.trigger > b.trigger end)
+ ]]
return list
end
local selectorBg = Material("sgm/playercircle")
local selected
+ local clicking = false
+ local open_btn
+
+
+ local clickable = eventwheel_clickable:GetInt() == 0 or eventwheel_clickable:GetInt() == 1
+ local close_click = eventwheel_clickable:GetInt() == -1 or eventwheel_clickable:GetInt() == 0
+
+ local clickable2 = eventlist_clickable:GetInt() == 0 or eventlist_clickable:GetInt() == 1
+ local close_click2 = eventlist_clickable:GetInt() == -1 or eventlist_clickable:GetInt() == 0
function pac.openEventSelectionWheel()
+ if not IsValid(open_btn) then open_btn = vgui.Create("DButton") end
+ open_btn:SetSize(80,30)
+ open_btn:SetText("Customize")
+ open_btn:SetPos(ScrW() - 80,0)
+
+ function open_btn:DoClick()
+
+ if (pace.command_event_menu_opened == nil) then
+ pace.ConfigureEventWheelMenu()
+ elseif IsValid(pace.command_event_menu_opened) then
+ pace.command_event_menu_opened:Remove()
+ end
+
+ end
+
+ if show_customize_button:GetBool() then
+ open_btn:Show()
+ else
+ open_btn:Hide()
+ end
+ pace.command_colors = pace.command_colors or {}
+ clickable = eventwheel_clickable:GetInt() == 0 or eventwheel_clickable:GetInt() == 1
+ close_click = eventwheel_clickable:GetInt() == -1 or eventwheel_clickable:GetInt() == 0
+
gui.EnableScreenClicker(true)
local scrw, scrh = ScrW(), ScrH()
@@ -2129,6 +4344,13 @@ do
local ply = pac.LocalPlayer
local data = ply.pac_command_events and ply.pac_command_events[self.event.trigger] and ply.pac_command_events[self.event.trigger]
+
+
+ local d1 = 64 --indicator
+
+
+ local d2 = 50 --color
+ local indicator_color
if data then
local is_oneshot = self.event.time and self.event.time > 0
@@ -2136,28 +4358,84 @@ do
local f = (pac.RealTime - data.time) / self.event.time
local s = Lerp(math.Clamp(f,0,1), 1, 0)
local v = Lerp(math.Clamp(f,0,1), 0.55, 0.15)
- surface.SetDrawColor(HSVToColor(210,s,v))
+ indicator_color = HSVToColor(210,s,v)
+
else
if data.on == 1 then
- surface.SetDrawColor(HSVToColor(210,1,0.55))
+ indicator_color = HSVToColor(210,1,0.55)
else
- surface.SetDrawColor(HSVToColor(210,0,0.15))
+ indicator_color = HSVToColor(210,0,0.15)
end
end
else
- surface.SetDrawColor(HSVToColor(210,0,0.15))
+ indicator_color = HSVToColor(210,0,0.15)
+ end
+
+ if eventwheel_style:GetInt() == 0 then
+ d2 = 96
+ surface.SetDrawColor(indicator_color)
+ surface.DrawTexturedRect(x-(d2/2), y-(d2/2), d2, d2)
+ elseif eventwheel_style:GetInt() == 1 then
+ if pace.command_colors[self.name] then
+ local col_str_tbl = string.Split(pace.command_colors[self.name]," ")
+ surface.SetDrawColor(tonumber(col_str_tbl[1]),tonumber(col_str_tbl[2]),tonumber(col_str_tbl[3]))
+ else
+ surface.SetDrawColor(HSVToColor(210,0,0.15))
+ end
+
+ d1 = 100 --color
+ d2 = 50 --indicator
+
+ surface.DrawTexturedRect(x-(d1/2), y-(d1/2), d1, d1)
+
+ surface.SetDrawColor(indicator_color)
+ surface.DrawTexturedRect(x-(d2/2), y-(d2/2), d2, d2)
+
+ draw.RoundedBox(0,x-40,y-8,80,16,Color(0,0,0))
+
+ elseif eventwheel_style:GetInt() == 2 then
+ if pace.command_colors[self.name] then
+ local col_str_tbl = string.Split(pace.command_colors[self.name]," ")
+ surface.SetDrawColor(tonumber(col_str_tbl[1]),tonumber(col_str_tbl[2]),tonumber(col_str_tbl[3]))
+ else
+ surface.SetDrawColor(HSVToColor(210,0,0.15))
+ end
+
+ d1 = 96 --color
+ d2 = 40 --indicator
+
+ surface.DrawTexturedRect(x-(d1/2), y-(d1/2), d1, d1)
+ surface.SetDrawColor(indicator_color)
+ surface.DrawTexturedRect(x-1.2*d2, y-1.2*d2, d2, d2)
end
- surface.DrawTexturedRect(x-48, y-48, 96, 96)
- draw.SimpleText(self.name, "DermaDefault", x, y, color_white, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
+ draw.SimpleText(self.name, eventwheel_font:GetString(), x, y, color_white, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
cam.PopModelMatrix()
end
pac.AddHook("HUDPaint","custom_event_selector",function()
-- Right clicking cancels
- if input.IsButtonDown(MOUSE_RIGHT) then pac.closeEventSelectionWheel(true) return end
+ if input.IsButtonDown(MOUSE_RIGHT) and not IsValid(pace.command_event_menu_opened) then pac.closeEventSelectionWheel(true) return end
+ if input.IsButtonDown(MOUSE_LEFT) and not pace.command_event_menu_opened and not open_btn:IsHovered() and clickable then
+
+ if not clicking and selected then
+ if not selected.event.time then
+ RunConsoleCommand("pac_event", selected.event.trigger, "toggle")
+ elseif selected.event.time > 0 then
+ RunConsoleCommand("pac_event", selected.event.trigger)
+ else
+ local ply = pac.LocalPlayer
+ if ply.pac_command_events and ply.pac_command_events[selected.event.trigger] and ply.pac_command_events[selected.event.trigger].on == 1 then
+ RunConsoleCommand("pac_event", selected.event.trigger, "0")
+ else
+ RunConsoleCommand("pac_event", selected.event.trigger, "1")
+ end
+ end
+ end
+ clicking = true
+ else clicking = false end
-- Normalize mouse vector from center of screen
local x, y = input.GetCursorPos()
x = x - scrw2
@@ -2181,13 +4459,17 @@ do
render.PopFilterMin()
DisableClipping(false)
end)
+ pace.event_wheel_opened = true
end
function pac.closeEventSelectionWheel(cancel)
+
+ if IsValid(pace.command_event_menu_opened) then return end
+ open_btn:Hide()
gui.EnableScreenClicker(false)
pac.RemoveHook("HUDPaint","custom_event_selector")
- if selected and cancel ~= true then
+ if selected and cancel ~= true and close_click then
if not selected.event.time then
RunConsoleCommand("pac_event", selected.event.trigger, "toggle")
elseif selected.event.time > 0 then
@@ -2203,8 +4485,292 @@ do
end
end
selected = nil
+ pace.event_wheel_opened = false
+ end
+
+ local panels = {}
+ function pac.openEventSelectionList()
+ if not IsValid(open_btn) then open_btn = vgui.Create("DButton") end
+ open_btn:SetSize(80,30)
+ open_btn:SetText("Customize")
+ open_btn:SetPos(ScrW() - 80,0)
+
+ function open_btn:DoClick()
+
+ if (pace.command_event_menu_opened == nil) then
+ pace.ConfigureEventWheelMenu()
+ elseif IsValid(pace.command_event_menu_opened) then
+ pace.command_event_menu_opened:Remove()
+ end
+
+ end
+
+ if show_customize_button:GetBool() then
+ open_btn:Show()
+ else
+ open_btn:Hide()
+ end
+ pace.command_colors = pace.command_colors or {}
+ clickable2 = eventlist_clickable:GetInt() == 0 or eventlist_clickable:GetInt() == 1
+ close_click2 = eventlist_clickable:GetInt() == -1 or eventlist_clickable:GetInt() == 0
+
+ local base_fontsize = tonumber(string.match(event_list_font:GetString(),"%d*$")) or 12
+ local height = 2*base_fontsize + 8
+ panels = panels or {}
+ if not table.IsEmpty(panels) then
+ for i, v in pairs(panels) do
+ v:Remove()
+ end
+ end
+ local selections = {}
+ local events = get_events()
+ for i, v in ipairs(events) do
+
+
+ local list_element = vgui.Create("DPanel")
+
+ panels[i] = list_element
+ list_element:SetSize(250,height)
+ list_element.event = v
+
+ selections[i] = {
+ grow = 0,
+ name = v.trigger,
+ event = v,
+ pnl = list_element
+ }
+ function list_element:Paint() end
+ function list_element:DoCommand()
+ if not selected.event.time then
+ RunConsoleCommand("pac_event", selected.event.trigger, "toggle")
+ elseif selected.event.time > 0 then
+ RunConsoleCommand("pac_event", selected.event.trigger)
+ else
+ local ply = pac.LocalPlayer
+
+ if ply.pac_command_events and ply.pac_command_events[selected.event.trigger] and ply.pac_command_events[selected.event.trigger].on == 1 then
+ RunConsoleCommand("pac_event", selected.event.trigger, "0")
+ else
+ RunConsoleCommand("pac_event", selected.event.trigger, "1")
+ end
+ end
+ end
+ function list_element:Think()
+ if input.IsKeyDown(KEY_LCONTROL) and input.IsKeyDown(KEY_G) then
+ self:Remove()
+ end
+ if self:IsHovered() then
+ selected = self
+ if input.IsMouseDown(MOUSE_LEFT) and not self.was_clicked and not IsValid(pace.command_event_menu_opened) and not open_btn:IsHovered() and clickable2 then
+ self.was_clicked = true
+ self:DoCommand()
+ elseif not input.IsMouseDown(MOUSE_LEFT) then self.was_clicked = false end
+ end
+ end
+
+ end
+
+ gui.EnableScreenClicker(true)
+
+ pac.AddHook("HUDPaint","custom_event_selector_list",function()
+ local base_fontsize = tonumber(string.match(event_list_font:GetString(),"%d*$")) or 12
+ local height = 2*base_fontsize + 8
+ -- Right clicking cancels
+ if input.IsButtonDown(MOUSE_RIGHT) and not IsValid(pace.command_event_menu_opened) then pac.closeEventSelectionList(true) return end
+
+ DisableClipping(true)
+ render.PushFilterMag(TEXFILTER.ANISOTROPIC)
+ render.PushFilterMin(TEXFILTER.ANISOTROPIC)
+ draw.SimpleText("Right click to cancel", "DermaDefault", ScrW()/2, ScrH()/2, color_red, TEXT_ALIGN_CENTER, TEXT_ALIGN_TOP)
+ local x = 0
+ local y = 0
+ for i, v in ipairs(selections) do
+ if IsValid(v.pnl) then
+
+ if y + height > ScrH() then
+ y = 0
+ x = x + 200
+ end
+ local list_element = v.pnl
+ list_element:SetPos(x,y)
+ list_element:SetSize(250,height)
+
+ local ply = pac.LocalPlayer
+ local data = ply.pac_command_events and ply.pac_command_events[list_element.event.trigger]
+ local indicator_color
+
+ if data then
+ local is_oneshot = list_element.event.time and list_element.event.time > 0
+
+ if is_oneshot then
+ local f = (pac.RealTime - data.time) / list_element.event.time
+ local s = Lerp(math.Clamp(f,0,1), 1, 0)
+ local v = Lerp(math.Clamp(f,0,1), 0.55, 0.15)
+
+ indicator_color = HSVToColor(210,s,v)
+ else
+ if data.on == 1 then
+ indicator_color = HSVToColor(210,1,0.55)
+ else
+ indicator_color = HSVToColor(210,0,0.15)
+ end
+ end
+ else
+ indicator_color = HSVToColor(210,0,0.15)
+ end
+
+ local main_color = HSVToColor(210,0,0.15)
+ if pace.command_colors[v.name] then
+ local col_str_tbl = string.Split(pace.command_colors[v.name]," ")
+ main_color = Color(tonumber(col_str_tbl[1]),tonumber(col_str_tbl[2]),tonumber(col_str_tbl[3]))
+ end
+
+ local hue, sat, lightness_value = ColorToHSL(main_color)
+
+
+ if eventlist_style:GetInt() == 0 then
+ surface.SetDrawColor(indicator_color)
+ surface.DrawRect(x,y,200,height)
+ surface.SetDrawColor(0,0,0)
+ surface.DrawOutlinedRect(x,y,200,height,2)
+ elseif eventlist_style:GetInt() == 1 then
+ if pace.command_colors[v.name] then
+ local col_str_tbl = string.Split(pace.command_colors[v.name]," ")
+ surface.SetDrawColor(tonumber(col_str_tbl[1]),tonumber(col_str_tbl[2]),tonumber(col_str_tbl[3]))
+ else
+ surface.SetDrawColor(HSVToColor(210,0,0.15))
+ end
+ surface.DrawRect(x,y,200,height)
+
+ surface.SetDrawColor(indicator_color)
+ surface.DrawRect(x + 200/6,y + height/6,200 * 0.666,height * 0.666,2)
+ surface.SetDrawColor(0,0,0)
+ surface.DrawOutlinedRect(x,y,200,height,2)
+
+ elseif eventlist_style:GetInt() == 2 then
+ surface.DrawOutlinedRect(x,y,200,height,2)
+ if pace.command_colors[v.name] then
+ local col_str_tbl = string.Split(pace.command_colors[v.name]," ")
+ surface.SetDrawColor(tonumber(col_str_tbl[1]),tonumber(col_str_tbl[2]),tonumber(col_str_tbl[3]))
+ else
+ surface.SetDrawColor(HSVToColor(210,0,0.15))
+ end
+ surface.DrawRect(x,y,200,height)
+
+ surface.SetDrawColor(indicator_color)
+ surface.DrawRect(x + 150,y,50,height/2,2)
+ surface.SetDrawColor(0,0,0)
+ surface.DrawOutlinedRect(x + 150,y,50,height/2,2)
+ surface.DrawOutlinedRect(x,y,200,height,2)
+ end
+
+ local text_color = Color(255,255,255)
+ if lightness_value > 0.5 and eventlist_style:GetInt() ~= 0 then
+ text_color = Color(0,0,0)
+ end
+ draw.SimpleText(v.name,event_list_font:GetString(),x + 4,y + 4, text_color, TEXT_ALIGN_LEFT)
+ y = y + height
+
+ end
+
+ end
+
+ render.PopFilterMag()
+ render.PopFilterMin()
+ DisableClipping(false)
+
+ end)
+
+ pace.event_wheel_list_opened = true
+ end
+
+ function pac.closeEventSelectionList(cancel)
+ if IsValid(pace.command_event_menu_opened) then return end
+ open_btn:Hide()
+ gui.EnableScreenClicker(false)
+ pac.RemoveHook("HUDPaint","custom_event_selector_list")
+
+ if IsValid(selected) and close_click2 and cancel ~= true then
+ if selected:IsHovered() then
+ selected:DoCommand()
+ end
+ end
+ for i,v in pairs(panels) do v:Remove() end
+ selected = nil
+ pace.event_wheel_list_opened = false
end
+
concommand.Add("+pac_events", pac.openEventSelectionWheel)
concommand.Add("-pac_events", pac.closeEventSelectionWheel)
+
+ concommand.Add("+pac_events_list", pac.openEventSelectionList)
+ concommand.Add("-pac_events_list", pac.closeEventSelectionList)
+
end
+
+
+net.Receive("pac_update_healthbars", function(len)
+ pac.healthmod_part_UID_caches = pac.healthmod_part_UID_caches or {}
+ local ent = net.ReadEntity()
+ if ent:EntIndex() == 0 then return end
+ pac.healthmod_part_UID_caches[ent] = pac.healthmod_part_UID_caches[ent] or {}
+ if not IsValid(ent) then return end
+ local layers = net.ReadUInt(4)
+ --local tbl = net.ReadTable()
+ local tbl = {}
+ for i=0,layers,1 do
+ local skip = net.ReadBool()
+ if skip then continue end
+ tbl[i] = {}
+ local number_parts = net.ReadUInt(4)
+ for j=1,number_parts,1 do
+ local partial_uid = net.ReadString()
+ local value = net.ReadUInt(24)
+
+ local cached_part = pac.healthmod_part_UID_caches[ent][partial_uid]
+ if cached_part then
+ tbl[i][cached_part.UniqueID] = value
+ end
+
+ end
+ end
+ --PrintTable(tbl)
+
+ ent.pac_healthbars = tbl
+
+ local previous_total = ent.pac_healthbars_total or 0
+ local previous_totals_layers = ent.pac_healthbars_layertotals or {}
+ local previous_totals_uids = ent.pac_healthbars_uidtotals or {}
+
+ ent.pac_healthbars_layertotals = {}
+ ent.pac_healthbars_uidtotals = {}
+ ent.pac_healthbars_total = 0
+ ent.pac_healthbars_total_updated = {time = CurTime(), delta = 0}
+ ent.pac_healthbars_layers_updated = {time = CurTime(), deltas = {}}
+ ent.pac_healthbars_uids_updated = {time = CurTime(), deltas = {}}
+
+ for layer=15,0,-1 do --go progressively inward in the layers
+ ent.pac_healthbars_layertotals[layer] = 0
+ if tbl[layer] then
+ for uid,value in pairs(tbl[layer]) do --check the healthbars by uid
+ value = math.Round(value) --so apparently some damage sources like dynamite can deal fractional damage
+ --dynamites made a giga ugly mess with decimals ruining my hud display
+ ent.pac_healthbars_uidtotals[uid] = value
+ ent.pac_healthbars_layertotals[layer] = ent.pac_healthbars_layertotals[layer] + value
+ ent.pac_healthbars_total = ent.pac_healthbars_total + value
+
+ ent.pac_healthbars_uids_updated.deltas[uid] = (previous_totals_uids[uid] or 0) - value
+
+ local part = pac.GetPartFromUniqueID(pac.Hash(ent), uid)
+ if IsValid(part) and part.UpdateHPBars then part:UpdateHPBars() end
+ end
+ ent.pac_healthbars_layers_updated.deltas[layer] = (previous_totals_layers[layer] or 0) - (ent.pac_healthbars_layertotals[layer] or 0)
+ else
+ ent.pac_healthbars_layertotals[layer] = nil
+ end
+ end
+
+ --delta is actually -delta but whatever
+ ent.pac_healthbars_total_updated.delta = previous_total - ent.pac_healthbars_total
+end)
diff --git a/lua/pac3/core/client/parts/force.lua b/lua/pac3/core/client/parts/force.lua
new file mode 100644
index 000000000..9ace6c4c7
--- /dev/null
+++ b/lua/pac3/core/client/parts/force.lua
@@ -0,0 +1,665 @@
+local BUILDER, PART = pac.PartTemplate("base_drawable")
+
+PART.ClassName = "force"
+PART.Group = "combat"
+PART.Icon = "icon16/database_go.png"
+
+PART.ManualDraw = true
+PART.HandleModifiersManually = true
+
+PART.ImplementsDoubleClickSpecified = true
+
+BUILDER:StartStorableVars()
+ :SetPropertyGroup("AreaShape")
+ :GetSet("HitboxMode", "Box", {enums = {
+ ["Box"] = "Box",
+ ["Cube"] = "Cube",
+ ["Sphere"] = "Sphere",
+ ["Cylinder"] = "Cylinder",
+ ["Cone"] = "Cone",
+ ["Ray"] = "Ray"
+ }})
+ :GetSet("Length", 50, {editor_onchange = function(self,num) return math.floor(math.Clamp(num,-32768,32767)) end})
+ :GetSet("Radius", 50, {editor_onchange = function(self,num) return math.floor(math.Clamp(num,-32768,32767)) end})
+ :GetSet("Preview",false, {description = "preview target selection boxes"})
+ :GetSet("PreviewForces",false, {description = "preview the predicted forces"})
+
+ :SetPropertyGroup("BaseForces")
+ :GetSet("BaseForce", 0)
+ :GetSet("AddedVectorForce", Vector(0,0,0))
+ :GetSet("Torque", Vector(0,0,0))
+ :GetSet("BaseForceAngleMode","Radial",{enums = {["Radial"] = "Radial", ["Locus"] = "Locus", ["Local"] = "Local"},
+ description =
+[[Radial points the base force outward from the force part. To point in, use negative values
+
+Locus points out from the locus (external point)
+
+Local points forward (red arrow) from the force part]]})
+ :GetSet("VectorForceAngleMode", "Global", {enums = {["Global"] = "Global", ["Local"] = "Local", ["Radial"] = "Radial", ["RadialNoPitch"] = "RadialNoPitch"},
+ description =
+[[Global applies the vector force on world coordinates
+
+Local applies it based on the force part's angles
+
+Radial gets the base directions from the targets to the force part
+
+RadialNoPitch gets the base directions from the targets to the force part, but making pitch horizon-level]]})
+ :GetSet("TorqueMode", "TargetLocal", {enums = {["Global"] = "Global", ["TargetLocal"] = "TargetLocal", ["Local"] = "Local", ["Radial"] = "Radial"},
+ description =
+[[Global applies the angular force on world coordinates
+
+TargetLocal applies it on the target's local angles
+
+Local applies it based on the force part's angles
+
+Radial gets the base directions from the targets to the force part]]})
+ :GetSetPart("Locus", nil)
+
+ :SetPropertyGroup("Behaviors")
+ :GetSet("Continuous", true, {description = "If set to false, the force will be a single, stronger impulse"})
+ :GetSet("AccountMass", false, {description = "Apply acceleration according to mass."})
+ :GetSet("Falloff", false, {description = "Whether the force to apply should fade with distance"})
+ :GetSet("ReverseFalloff", false, {description = "The reverse of the falloff means the force fades when getting closer."})
+ :GetSet("Levitation", false, {description = "Tries to stabilize the force to levitate targets at a certain height relative to the part.\nRequires vertical forces. Easiest way is to enter 0 0 500 in 'added vector force' with the Global vector mode which is already there by default."})
+ :GetSet("LevitationHeight", 0)
+
+ :SetPropertyGroup("Damping")
+ :GetSet("Damping", 0, {editor_clamp = {0,1}, editor_sensitivity = 0.1, description = "Reduces the existing velocity before applying force, by way of multiplication by (1-damping). 0 doesn't change it, while 1 is a full negation of the initial speed."})
+ :GetSet("DampingFalloff", false, {description = "Whether the damping should fade with distance (further is weaker influence)"})
+ :GetSet("DampingReverseFalloff", false, {description = "Whether the damping should fade with distance but reverse (closer is weaker influence)"})
+
+ :SetPropertyGroup("Targets")
+ :GetSet("AffectSelf", false, {description = "Affect Root Owner (i.e. it can be a projectile entity)"})
+ :GetSet("AlternateAffectSelf", false, {description = "'Affect Self' was split into two. For backward compatibility reasons, this must be a setting.\nif this is off, affect self will affect both player owner and root owner, which may not be desired"})
+ :GetSet("AffectPlayerOwner", false)
+ :GetSet("Players",true)
+ :GetSet("PhysicsProps", true)
+ :GetSet("PointEntities",true, {description = "other entities not covered by physics props but with potential physics"})
+ :GetSet("NPC",false)
+:EndStorableVars()
+
+local force_hitbox_ids = {["Box"] = 0,["Cube"] = 1,["Sphere"] = 2,["Cylinder"] = 3,["Cone"] = 4,["Ray"] = 5}
+local base_force_mode_ids = {["Radial"] = 0, ["Locus"] = 1, ["Local"] = 2}
+local vect_force_mode_ids = {["Global"] = 0, ["Local"] = 1, ["Radial"] = 2, ["RadialNoPitch"] = 3}
+local ang_torque_mode_ids = {["Global"] = 0, ["TargetLocal"] = 1, ["Local"] = 2, ["Radial"] = 3}
+
+function PART:OnRemove()
+end
+
+function PART:Initialize()
+ self.next_impulse = CurTime() + 0.05
+ if not GetConVar("pac_sv_force"):GetBool() or pac.Blocked_Combat_Parts[self.ClassName] then self:SetError("force parts are disabled on this server!") end
+ timer.Simple(0, function()
+ self.initialized = true
+ end)
+end
+
+function PART:OnShow()
+ self.next_impulse = CurTime() + 0.05
+ self:Impulse(true)
+end
+
+function PART:OnDoubleClickSpecified()
+ self:Impulse(true)
+end
+
+function PART:OnHide()
+ pac.RemoveHook("PostDrawOpaqueRenderables", "pac_force_Draw"..self.UniqueID)
+ self:Impulse(false)
+end
+
+function PART:OnRemove()
+ pac.RemoveHook("PostDrawOpaqueRenderables", "pac_force_Draw"..self.UniqueID)
+ self:Impulse(false)
+end
+
+
+local white = Color(255,255,255)
+local red = Color(255,0,0)
+local green = Color(0,255,0)
+local blue = Color(0,0,255)
+local red2 = Color(255,100,100)
+local red3 = Color(255,200,200)
+local function draw_force_line(pos, amount)
+ local length = amount:Length()
+ local magnitude = length / 20
+ amount:Normalize()
+ local x = amount.x
+ local y = amount.y
+ local z = amount.z
+ local dir = amount:Angle()
+ render.DrawLine( pos, 9 * magnitude * x * Vector(1,0,0) + pos, red, false)
+ render.DrawLine( pos, 9 * magnitude * y * Vector(0,1,0) + pos, green, false)
+ render.DrawLine( pos, 9 * magnitude * z * Vector(0,0,1) + pos, blue, false)
+ cam.IgnoreZ( true )
+ for i=0,8,1 do
+ local scrolling = -i + math.floor((CurTime() % 1) * 8) + 2
+ if scrolling == 0 then
+ render.DrawLine( (i) * magnitude * amount + pos, (i+1) * magnitude * amount + pos, red, false)
+ elseif scrolling == 1 then
+ render.DrawLine( (i) * magnitude * amount + pos, (i+1) * magnitude * amount + pos, red2, false)
+ elseif scrolling == 2 then
+ render.DrawLine( (i) * magnitude * amount + pos, (i+1) * magnitude * amount + pos, red3, false)
+ else
+ render.DrawLine( (i) * magnitude * amount + pos, (i+1) * magnitude * amount + pos, white, false)
+ end
+
+ end
+ cam.IgnoreZ( false )
+end
+
+
+--convenience functions and tables from net_combat
+
+local pre_excluded_ent_classes = {
+ ["info_player_start"] = true,
+ ["aoc_spawnpoint"] = true,
+ ["info_player_teamspawn"] = true,
+ ["env_tonemap_controller"] = true,
+ ["env_fog_controller"] = true,
+ ["env_skypaint"] = true,
+ ["shadow_control"] = true,
+ ["env_sun"] = true,
+ ["predicted_viewmodel"] = true,
+ ["physgun_beam"] = true,
+ ["ambient_generic"] = true,
+ ["trigger_once"] = true,
+ ["trigger_multiple"] = true,
+ ["trigger_hurt"] = true,
+ ["info_ladder_dismount"] = true,
+ ["info_particle_system"] = true,
+ ["env_sprite"] = true,
+ ["env_fire"] = true,
+ ["env_soundscape"] = true,
+ ["env_smokestack"] = true,
+ ["light"] = true,
+ ["move_rope"] = true,
+ ["keyframe_rope"] = true,
+ ["env_soundscape_proxy"] = true,
+ ["gmod_hands"] = true,
+ ["env_lightglow"] = true,
+ ["point_spotlight"] = true,
+ ["spotlight_end"] = true,
+ ["beam"] = true,
+ ["info_target"] = true,
+ ["func_lod"] = true,
+ ["func_brush"] = true,
+ ["phys_bone_follower"] = true,
+}
+
+local physics_point_ent_classes = {
+ ["prop_physics"] = true,
+ ["prop_physics_multiplayer"] = true,
+ ["prop_ragdoll"] = true,
+ ["weapon_striderbuster"] = true,
+ ["item_item_crate"] = true,
+ ["func_breakable_surf"] = true,
+ ["func_breakable"] = true,
+ ["physics_cannister"] = true
+}
+
+local function MergeTargetsByID(tbl1, tbl2)
+ for i,v in ipairs(tbl2) do
+ tbl1[v:EntIndex()] = v
+ end
+end
+
+local function Is_NPC(ent)
+ return ent:IsNPC() or ent:IsNextBot() or ent.IsDrGEntity or ent.IsVJBaseSNPC
+end
+
+local function ProcessForcesList(ents_hits, tbl, pos, ang, ply)
+ for i,v in pairs(ents_hits) do
+ if pre_excluded_ent_classes[v:GetClass()] then ents_hits[i] = nil end
+ end
+ local ftime = 0.016 --approximate tick duration
+ local BASEFORCE = 0
+ local VECFORCE = Vector(0,0,0)
+ if tbl.Continuous then
+ BASEFORCE = tbl.BaseForce * ftime * 3.3333 --weird value to equalize how 600 cancels out gravity
+ VECFORCE = tbl.AddedVectorForce * ftime * 3.3333
+ else
+ BASEFORCE = tbl.BaseForce
+ VECFORCE = tbl.AddedVectorForce
+ end
+ for _,ent in pairs(ents_hits) do
+ if ent:IsWeapon() or ent:GetClass() == "viewmodel" or ent:GetClass() == "func_physbox_multiplayer" then continue end
+ local phys_ent
+ local is_player = ent:IsPlayer()
+ local is_physics = (physics_point_ent_classes[ent:GetClass()] or string.find(ent:GetClass(),"item_") or string.find(ent:GetClass(),"ammo_") or (ent:IsWeapon() and not IsValid(ent:GetOwner())))
+ local is_npc = Is_NPC(ent)
+
+ if is_npc and not tbl.NPC then continue end
+ if is_player and not (tbl.Players or ent == tbl:GetPlayerOwner()) then continue end
+ if is_player and not tbl.AffectSelf and ent == tbl:GetPlayerOwner() then continue end
+ if is_physics and not tbl.PhysicsProps then continue end
+ if not is_npc and not is_player and not is_physics then
+ if not tbl.PointEntities then continue end
+ end
+
+ local is_phys = true
+ phys_ent = ent
+ is_phys = false
+
+ local oldvel
+
+ if IsValid(phys_ent) then
+ oldvel = phys_ent:GetVelocity()
+ else
+ oldvel = Vector(0,0,0)
+ end
+
+
+ local addvel = Vector(0,0,0)
+ local add_angvel = Vector(0,0,0)
+
+ local ent_center = ent:WorldSpaceCenter() or ent:GetPos()
+
+ local dir = ent_center - pos --part
+ local locus_pos = pos
+ if tbl.Locus ~= nil then
+ if tbl.Locus:IsValid() then
+ locus_pos = tbl.Locus:GetWorldPosition()
+ end
+ end
+ local dir2 = ent_center - locus_pos
+
+ local dist_multiplier = 1
+ local damping_dist_mult = 1
+ local up_mult = 1
+ local distance = (ent_center - pos):Length()
+ local height_delta = pos.z + tbl.LevitationHeight - ent_center.z
+
+ --what it do
+ --if delta is -100 (ent is lower than the desired height), that means +100 adjustment direction
+ --height decides how much to knee the force until it equalizes at 0
+ --clamp the delta to the ratio levitation height
+
+ if tbl.Levitation then
+ up_mult = math.Clamp(height_delta / (5 + math.abs(tbl.LevitationHeight)),-1,1)
+ end
+
+ if tbl.BaseForceAngleMode == "Radial" then --radial on self
+ addvel = dir:GetNormalized() * tbl.BaseForce
+ elseif tbl.BaseForceAngleMode == "Locus" then --radial on locus
+ addvel = dir2:GetNormalized() * tbl.BaseForce
+ elseif tbl.BaseForceAngleMode == "Local" then --forward on self
+ addvel = ang:Forward() * tbl.BaseForce
+ end
+
+ if tbl.VectorForceAngleMode == "Global" then --global
+ addvel = addvel + tbl.AddedVectorForce
+ elseif tbl.VectorForceAngleMode == "Local" then --local on self
+ addvel = addvel
+ +ang:Forward()*tbl.AddedVectorForce.x
+ +ang:Right()*tbl.AddedVectorForce.y
+ +ang:Up()*tbl.AddedVectorForce.z
+
+ elseif tbl.VectorForceAngleMode == "Radial" then --relative to locus or self
+ ang2 = dir:Angle()
+ addvel = addvel
+ +ang2:Forward()*tbl.AddedVectorForce.x
+ +ang2:Right()*tbl.AddedVectorForce.y
+ +ang2:Up()*tbl.AddedVectorForce.z
+ elseif tbl.VectorForceAngleMode == "RadialNoPitch" then --relative to locus or self
+ dir.z = 0
+ ang2 = dir:Angle()
+ addvel = addvel
+ +ang2:Forward()*tbl.AddedVectorForce.x
+ +ang2:Right()*tbl.AddedVectorForce.y
+ +ang2:Up()*tbl.AddedVectorForce.z
+ end
+
+ --[[if tbl.TorqueMode == "Global" then
+ add_angvel = tbl.Torque
+ elseif tbl.TorqueMode == "Local" then
+ add_angvel = ang:Forward()*tbl.Torque.x + ang:Right()*tbl.Torque.y + ang:Up()*tbl.Torque.z
+ elseif tbl.TorqueMode == "TargetLocal" then
+ add_angvel = tbl.Torque
+ elseif tbl.TorqueMode == "Radial" then
+ ang2 = dir:Angle()
+ addvel = ang2:Forward()*tbl.Torque.x + ang2:Right()*tbl.Torque.y + ang2:Up()*tbl.Torque.z
+ end]]
+
+ local mass = 1
+ if IsValid(phys_ent) then
+ if phys_ent.GetMass then
+ phys_ent:GetMass()
+ end
+ end
+ if is_phys and tbl.AccountMass then
+ if not is_npc then
+ addvel = addvel * (1 / math.max(mass,0.1))
+ else
+ addvel = addvel
+ end
+ add_angvel = add_angvel * (1 / math.max(mass,0.1))
+ end
+
+ if tbl.Falloff then
+ dist_multiplier = math.Clamp(1 - distance / math.max(tbl.Radius, tbl.Length),0,1)
+ end
+ if tbl.ReverseFalloff then
+ dist_multiplier = 1 - math.Clamp(1 - distance / math.max(tbl.Radius, tbl.Length),0,1)
+ end
+
+ if tbl.DampingFalloff then
+ damping_dist_mult = math.Clamp(1 - distance / math.max(tbl.Radius, tbl.Length),0,1)
+ end
+ if tbl.DampingReverseFalloff then
+ damping_dist_mult = 1 - math.Clamp(1 - distance / math.max(tbl.Radius, tbl.Length),0,1)
+ end
+ damping_dist_mult = damping_dist_mult
+ local final_damping = 1 - (tbl.Damping * damping_dist_mult)
+
+ if tbl.Levitation then
+ addvel.z = addvel.z * up_mult
+ end
+
+ addvel = addvel * dist_multiplier
+ draw_force_line(ent:WorldSpaceCenter(), addvel)
+
+ end
+end
+
+local function preview_process_ents(tbl)
+ ply = tbl:GetPlayerOwner()
+ local pos = tbl.pos
+ local ang = tbl.ang
+
+ if tbl.HitboxMode == "Sphere" then
+ local ents_hits = ents.FindInSphere(pos, tbl.Radius)
+ ProcessForcesList(ents_hits, tbl, pos, ang, ply)
+ elseif tbl.HitboxMode == "Box" then
+ local mins
+ local maxs
+ if tbl.HitboxMode == "Box" then
+ mins = pos - Vector(tbl.Radius, tbl.Radius, tbl.Length)
+ maxs = pos + Vector(tbl.Radius, tbl.Radius, tbl.Length)
+ end
+
+ local ents_hits = ents.FindInBox(mins, maxs)
+ ProcessForcesList(ents_hits, tbl, pos, ang, ply)
+ elseif tbl.HitboxMode == "Cylinder" then
+ local ents_hits = {}
+ if tbl.Length ~= 0 and tbl.Radius ~= 0 then
+ local counter = 0
+ MergeTargetsByID(ents_hits,ents.FindInSphere(pos, tbl.Radius))
+ for i=0,1,1/(math.abs(tbl.Length/tbl.Radius)) do
+ MergeTargetsByID(ents_hits,ents.FindInSphere(pos + ang:Forward()*tbl.Length*i, tbl.Radius))
+ if counter == 200 then break end
+ counter = counter + 1
+ end
+ MergeTargetsByID(ents_hits,ents.FindInSphere(pos + ang:Forward()*tbl.Length, tbl.Radius))
+ --render.DrawWireframeSphere( self:GetWorldPosition() + self:GetWorldAngles():Forward()*(self.Length - 0.5*self.Radius), 0.5*self.Radius, 10, 10, Color( 255, 255, 255 ) )
+ elseif tbl.Radius == 0 then MergeTargetsByID(ents_hits,ents.FindAlongRay(pos, pos + ang:Forward()*tbl.Length)) end
+ ProcessForcesList(ents_hits, tbl, pos, ang, ply)
+ elseif tbl.HitboxMode == "Cone" then
+ local ents_hits = {}
+ local steps
+ steps = math.Clamp(4*math.ceil(tbl.Length / (tbl.Radius or 1)),1,50)
+ for i = 1,0,-1/steps do
+ MergeTargetsByID(ents_hits,ents.FindInSphere(pos + ang:Forward()*tbl.Length*i, i * tbl.Radius))
+ end
+
+ steps = math.Clamp(math.ceil(tbl.Length / (tbl.Radius or 1)),1,4)
+
+ if tbl.Radius == 0 then MergeTargetsByID(ents_hits,ents.FindAlongRay(pos, pos + ang:Forward()*tbl.Length)) end
+ ProcessForcesList(ents_hits, tbl, pos, ang, ply)
+ elseif tbl.HitboxMode =="Ray" then
+ local startpos = pos + Vector(0,0,0)
+ local endpos = pos + ang:Forward()*tbl.Length
+ ents_hits = ents.FindAlongRay(startpos, endpos)
+ ProcessForcesList(ents_hits, tbl, pos, ang, ply)
+ end
+end
+
+function PART:OnDraw()
+ self.pos,self.ang = self:GetDrawPosition()
+ if not self.Preview and not self.PreviewForces then pac.RemoveHook("PostDrawOpaqueRenderables", "pac_force_Draw"..self.UniqueID) end
+
+ if self.Preview or self.PreviewForces then
+ pac.AddHook("PostDrawOpaqueRenderables", "pac_force_Draw"..self.UniqueID, function()
+ if self.PreviewForces then
+ --recalculating forces every drawframe is cringe for other players
+ if self:GetPlayerOwner() == pac.LocalPlayer then
+ if self.NPC or self.Players or self.AffectSelf or self.PhysicsProps or self.PointEntities then
+ preview_process_ents(self)
+ end
+ end
+ end
+ if not self.Preview then return end
+
+ if self.HitboxMode == "Box" then
+ local mins = Vector(-self.Radius, -self.Radius, -self.Length)
+ local maxs = Vector(self.Radius, self.Radius, self.Length)
+ render.DrawWireframeBox( self:GetWorldPosition(), Angle(0,0,0), mins, maxs, Color( 255, 255, 255 ) )
+ elseif self.HitboxMode == "Sphere" then
+ render.DrawWireframeSphere( self:GetWorldPosition(), self.Radius, 10, 10, Color( 255, 255, 255 ) )
+ elseif self.HitboxMode == "Cylinder" then
+ local obj = Mesh()
+ self:BuildCylinder(obj)
+ render.SetMaterial( Material( "models/wireframe" ) )
+ mat = Matrix()
+ mat:Translate(self:GetWorldPosition())
+ mat:Rotate(self:GetWorldAngles())
+ cam.PushModelMatrix( mat )
+ obj:Draw()
+ cam.PopModelMatrix()
+ if self.Length ~= 0 and self.Radius ~= 0 then
+ local counter = 0
+ --render.DrawWireframeSphere( self:GetWorldPosition(), self.Radius, 10, 10, Color( 255, 255, 255 ) )
+ for i=0,1,1/(math.abs(self.Length/self.Radius)) do
+ render.DrawWireframeSphere( self:GetWorldPosition() + self:GetWorldAngles():Forward()*self.Length*i, self.Radius, 10, 10, Color( 255, 255, 255 ) )
+ if counter == 200 then break end
+ counter = counter + 1
+ end
+ render.DrawWireframeSphere( self:GetWorldPosition() + self:GetWorldAngles():Forward()*(self.Length), self.Radius, 10, 10, Color( 255, 255, 255 ) )
+ elseif self.Radius == 0 then
+ render.DrawLine( self:GetWorldPosition(), self:GetWorldPosition() + self:GetWorldAngles():Forward()*self.Length, Color( 255, 255, 255 ), false )
+ end
+ elseif self.HitboxMode == "Cone" then
+ local obj = Mesh()
+ self:BuildCone(obj)
+ render.SetMaterial( Material( "models/wireframe" ) )
+ mat = Matrix()
+ mat:Translate(self:GetWorldPosition())
+ mat:Rotate(self:GetWorldAngles())
+ cam.PushModelMatrix( mat )
+ obj:Draw()
+ cam.PopModelMatrix()
+ if self.Radius ~= 0 then
+ local steps
+ steps = math.Clamp(4*math.ceil(self.Length / (self.Radius or 1)),1,50)
+ for i = 1,0,-1/steps do
+ render.DrawWireframeSphere( self:GetWorldPosition() + self:GetWorldAngles():Forward()*self.Length*i, i * self.Radius, 10, 10, Color( 255, 255, 255 ) )
+ end
+
+ steps = math.Clamp(math.ceil(self.Length / (self.Radius or 1)),1,4)
+ for i = 0,1/8,1/128 do
+ render.DrawWireframeSphere( self:GetWorldPosition() + self:GetWorldAngles():Forward()*self.Length*i, i * self.Radius, 10, 10, Color( 255, 255, 255 ) )
+ end
+ elseif self.Radius == 0 then
+ render.DrawLine( self:GetWorldPosition(), self:GetWorldPosition() + self:GetWorldAngles():Forward()*self.Length, Color( 255, 255, 255 ), false )
+ end
+ elseif self.HitboxMode == "Ray" then
+ render.DrawLine( self:GetWorldPosition(), self:GetWorldPosition() + self:GetWorldAngles():Forward()*self.Length, Color( 255, 255, 255 ), false )
+ end
+ end)
+ end
+end
+
+
+
+function PART:OnThink()
+ if self.Continuous and self.next_impulse < CurTime() then
+ self:Impulse(true)
+ end
+end
+
+function PART:Impulse(on)
+ self.next_impulse = CurTime() + 0.05
+ if pac.LocalPlayer ~= self:GetPlayerOwner() then return end
+ if not on and not self.Continuous then return end
+ if not GetConVar("pac_sv_force"):GetBool() then return end
+ if util.NetworkStringToID( "pac_request_force" ) == 0 then self:SetError("This part is deactivated on the server") return end
+ pac.Blocked_Combat_Parts = pac.Blocked_Combat_Parts or {}
+ if pac.Blocked_Combat_Parts then
+ if pac.Blocked_Combat_Parts[self.ClassName] then return end
+ end
+ if not GetConVar("pac_sv_combat_enforce_netrate_monitor_serverside"):GetBool() then
+ if not pac.CountNetMessage() then self:SetInfo("Went beyond the allowance") return end
+ end
+
+ if not self.initialized then return end
+
+ local locus_pos = Vector(0,0,0)
+ if self.Locus ~= nil then
+ if self.Locus:IsValid() then
+ locus_pos = self.Locus:GetWorldPosition()
+ end
+ else locus_pos = self:GetWorldPosition() end
+
+ if self.BaseForce == 0 and not game.SinglePlayer() and self.Damping == 0 then
+ if math.abs(self.AddedVectorForce.x) < 10 and math.abs(self.AddedVectorForce.y) < 10 and math.abs(self.AddedVectorForce.z) < 10 then
+ if math.abs(self.Torque.x) < 10 and math.abs(self.Torque.y) < 10 and math.abs(self.Torque.z) < 10 then
+ return
+ end
+ end
+ end
+
+ if not self.NPC and not self.Players and not self.AffectSelf and not self.AffectPlayerOwner and not self.PhysicsProps and not self.PointEntities then return end
+
+ net.Start("pac_request_force", true)
+ net.WriteVector(self:GetWorldPosition())
+ net.WriteAngle(self:GetWorldAngles())
+ net.WriteVector(locus_pos)
+ net.WriteBool(on)
+
+ net.WriteString(string.sub(self.UniqueID,1,12))
+ net.WriteEntity(self:GetRootPart():GetOwner())
+
+ net.WriteUInt(force_hitbox_ids[self.HitboxMode] or 0,4)
+ net.WriteUInt(base_force_mode_ids[self.BaseForceAngleMode] or 0,3)
+ net.WriteUInt(vect_force_mode_ids[self.VectorForceAngleMode] or 0,2)
+ net.WriteUInt(ang_torque_mode_ids[self.TorqueMode] or 0,2)
+
+ net.WriteInt(self.Length, 16)
+ net.WriteInt(self.Radius, 16)
+
+ net.WriteInt(self.BaseForce, 18)
+ net.WriteVector(self.AddedVectorForce)
+ net.WriteVector(self.Torque)
+ net.WriteUInt(self.Damping*1000, 10)
+ net.WriteInt(self.LevitationHeight,14)
+
+ net.WriteBool(self.Continuous)
+ net.WriteBool(self.AccountMass)
+ net.WriteBool(self.Falloff)
+ net.WriteBool(self.ReverseFalloff)
+ net.WriteBool(self.DampingFalloff)
+ net.WriteBool(self.DampingReverseFalloff)
+ net.WriteBool(self.Levitation)
+ if self.AlternateAffectSelf then
+ net.WriteBool(self.AffectSelf)
+ net.WriteBool(self.AffectPlayerOwner)
+ else
+ net.WriteBool(self.AffectSelf)
+ net.WriteBool(self.AffectPlayerOwner or self.AffectSelf)
+ end
+
+ net.WriteBool(self.Players)
+ net.WriteBool(self.PhysicsProps)
+ net.WriteBool(self.PointEntities)
+ net.WriteBool(self.NPC)
+ net.SendToServer()
+end
+
+
+
+function PART:BuildCylinder(obj)
+ local sides = 30
+ local circle_tris = {}
+ for i=1,sides,1 do
+ local vert1 = {pos = Vector(0, self.Radius*math.sin((i-1)*(2*math.pi / sides)),self.Radius*math.cos((i-1)*(2*math.pi / sides))), u = 0, v = 0 }
+ local vert2 = {pos = Vector(0, self.Radius*math.sin((i-0)*(2*math.pi / sides)),self.Radius*math.cos((i-0)*(2*math.pi / sides))), u = 0, v = 0 }
+ local vert3 = {pos = Vector(self.Length,self.Radius*math.sin((i-1)*(2*math.pi / sides)),self.Radius*math.cos((i-1)*(2*math.pi / sides))), u = 0, v = 0 }
+ local vert4 = {pos = Vector(self.Length,self.Radius*math.sin((i-0)*(2*math.pi / sides)),self.Radius*math.cos((i-0)*(2*math.pi / sides))), u = 0, v = 0 }
+
+ table.insert(circle_tris, vert1)
+ table.insert(circle_tris, vert2)
+ table.insert(circle_tris, vert3)
+
+ table.insert(circle_tris, vert3)
+ table.insert(circle_tris, vert2)
+ table.insert(circle_tris, vert1)
+
+ table.insert(circle_tris, vert4)
+ table.insert(circle_tris, vert3)
+ table.insert(circle_tris, vert2)
+
+ table.insert(circle_tris, vert2)
+ table.insert(circle_tris, vert3)
+ table.insert(circle_tris, vert4)
+
+ end
+ obj:BuildFromTriangles( circle_tris )
+end
+
+function PART:BuildCone(obj)
+ local sides = 30
+ local circle_tris = {}
+ local verttip = {pos = Vector(0,0,0), u = 0, v = 0 }
+ for i=1,sides,1 do
+ local vert1 = {pos = Vector(self.Length,self.Radius*math.sin((i-1)*(2*math.pi / sides)),self.Radius*math.cos((i-1)*(2*math.pi / sides))), u = 0, v = 0 }
+ local vert2 = {pos = Vector(self.Length,self.Radius*math.sin((i-0)*(2*math.pi / sides)),self.Radius*math.cos((i-0)*(2*math.pi / sides))), u = 0, v = 0 }
+
+ table.insert(circle_tris, verttip)
+ table.insert(circle_tris, vert1)
+ table.insert(circle_tris, vert2)
+
+ table.insert(circle_tris, vert2)
+ table.insert(circle_tris, vert1)
+ table.insert(circle_tris, verttip)
+
+ --circle_tris[8*(i-1) + 1] = vert1
+ --circle_tris[8*(i-1) + 2] = vert2
+ --circle_tris[8*(i-1) + 3] = vert3
+ --circle_tris[8*(i-1) + 4] = vert4
+ --circle_tris[8*(i-1) + 5] = vert3
+ --circle_tris[8*(i-1) + 6] = vert2
+ end
+ obj:BuildFromTriangles( circle_tris )
+end
+
+function PART:SetRadius(val)
+ self.Radius = val
+ local sv_dist = GetConVar("pac_sv_force_max_radius"):GetInt()
+ if self.Radius > sv_dist then
+ self:SetInfo("Your radius is beyond the server's maximum permitted! Server max is " .. sv_dist)
+ else
+ self:SetInfo(nil)
+ end
+end
+
+function PART:SetLength(val)
+ self.Length = val
+ local sv_dist = GetConVar("pac_sv_force_max_length"):GetInt()
+ if self.Length > sv_dist then
+ self:SetInfo("Your length is beyond the server's maximum permitted! Server max is " .. sv_dist)
+ else
+ self:SetInfo(nil)
+ end
+end
+
+function PART:SetBaseForce(val)
+ self.BaseForce = val
+ local sv_max = GetConVar("pac_sv_force_max_amount"):GetInt()
+ if self.BaseForce > sv_max then
+ self:SetInfo("Your base force is beyond the server's maximum permitted! Server max is " .. sv_max)
+ else
+ self:SetInfo(nil)
+ end
+end
+
+BUILDER:Register()
\ No newline at end of file
diff --git a/lua/pac3/core/client/parts/group.lua b/lua/pac3/core/client/parts/group.lua
index 2c0d2170a..ba6b30070 100644
--- a/lua/pac3/core/client/parts/group.lua
+++ b/lua/pac3/core/client/parts/group.lua
@@ -8,6 +8,8 @@ PART.Description = "right click to add parts"
BUILDER:StartStorableVars()
BUILDER:GetSet("Duplicate", false)
BUILDER:GetSet("OwnerName", "self")
+ BUILDER:GetSet("ModelTracker", "", {hide_in_editor = true})
+ BUILDER:GetSet("ClassTracker", "", {hide_in_editor = true})
BUILDER:EndStorableVars()
local init_list = {}
@@ -50,6 +52,8 @@ function PART:SetOwner(ent)
if not pac.HookEntityRender(owner, self) then
self:ShowFromRendering()
end
+ self.ModelTracker = owner:GetModel()
+ self.ClassTracker = owner:GetClass()
end
end
end
diff --git a/lua/pac3/core/client/parts/health_modifier.lua b/lua/pac3/core/client/parts/health_modifier.lua
new file mode 100644
index 000000000..7d6a69434
--- /dev/null
+++ b/lua/pac3/core/client/parts/health_modifier.lua
@@ -0,0 +1,320 @@
+local BUILDER, PART = pac.PartTemplate("base")
+
+PART.ClassName = "health_modifier"
+
+PART.Group = "combat"
+PART.Icon = "icon16/heart.png"
+
+BUILDER:StartStorableVars()
+ BUILDER:GetSet("ActivateOnShow", true)
+ BUILDER:GetSet("ActivateOnWear", true)
+
+ BUILDER:SetPropertyGroup("Health")
+ BUILDER:GetSet("ChangeHealth", false)
+ BUILDER:GetSet("FollowHealth", true, {description = "whether changing the max health should try to set your health at the same time"})
+ BUILDER:GetSet("MaxHealth", 100, {editor_onchange = function(self,num) return math.floor(math.Clamp(num,0,math.huge)) end})
+
+ BUILDER:SetPropertyGroup("ExtraHpBars")
+ BUILDER:GetSet("FollowHealthBars", true, {description = "whether changing the extra health bars should try to update them at the same time"})
+ BUILDER:GetSet("HealthBars", 0, {editor_onchange = function(self,num) return math.floor(math.Clamp(num,0,100)) end, description = "Extra health bars taking damage before the main health.\nThey work as multiple bars for convenience. The total will be bars * amount."})
+ BUILDER:GetSet("BarsAmount", 100, {editor_onchange = function(self,num) return math.floor(math.Clamp(num,0,math.huge)) end, description = "Extra health bars taking damage before the main health.\nThey work as multiple bars for convenience. The total will be bars * amount."})
+ BUILDER:GetSet("BarsLayer", 1, {editor_onchange = function(self,num) return math.floor(math.Clamp(num,0,15)) end, description = "The layer decides which bars get damaged first. Outer layers are bigger numbers."})
+ BUILDER:GetSet("AbsorbFactor", 0, {editor_onchange = function(self,num) return math.Clamp(num,-1,1) end, description = "How much damage to extra health bars should carry over to the main health. 1 is ineffective, 0 is normal, -1 is a healing conversion."})
+ BUILDER:GetSet("HPBarsResetOnHide", false)
+ BUILDER:GetSet("CountedHits", false, {description = "Instead of a quantity of HP points, make counted hits as a number.\nIt will spend 1 unit of the healthbar per hit."})
+ BUILDER:GetSet("NoOverflow", false, {description = "When shield breaks, remaining damage will be forgiven.\nIt won't affect the main health or any remaining healthbar."})
+
+ BUILDER:SetPropertyGroup("Armor")
+ BUILDER:GetSet("ChangeArmor", false)
+ BUILDER:GetSet("FollowArmor", true, {description = "whether changing the max armor should try to set your armor at the same time"})
+ BUILDER:GetSet("MaxArmor", 100, {editor_onchange = function(self,num) return math.floor(math.Clamp(num,0,math.huge)) end})
+
+ BUILDER:SetPropertyGroup("DamageMultipliers")
+ BUILDER:GetSet("DamageMultiplier", 1, {description = "Damage multiplier to the hits you take. They stack but might not help with hardcoded addons that directly edit your HP or something."})
+ BUILDER:GetSet("ModifierId", "", {description = "Putting an ID lets you update a damage multiplier from multiple health modifier parts so they don't stack, without using proxies."})
+ BUILDER:GetSet("MultiplierResetOnHide", false)
+
+BUILDER:EndStorableVars()
+
+pac.healthmod_part_UID_caches = {}
+--wait a minute can we just assume uids will be unique? what if people give each other pacs, the uids will be the same
+local function register_UID(self, str, ply)
+ pac.healthmod_part_UID_caches[ply] = pac.healthmod_part_UID_caches[ply] or {}
+ pac.healthmod_part_UID_caches[ply][str] = self
+end
+
+function PART:GetNiceName()
+ ply = self:GetPlayerOwner()
+ local str = "health_modifier"
+
+ if self.DamageMultiplier ~= 1 then
+ str = str .. " [dmg " .. self.DamageMultiplier .. "x]"
+ end
+
+ if self.ChangeHealth then
+ if ply:Health() ~= self.MaxHealth then
+ str = str .. " [" .. ply:Health() .. " / " .. self.MaxHealth .. " health]"
+ else
+ str = str .. " [" .. self.MaxHealth .. " health]"
+ end
+ end
+
+ if self.ChangeArmor then
+ if ply:Armor() ~= self.MaxArmor then
+ str = str .. " [" .. ply:Armor() .. " / " .. self.MaxArmor .. " armor]"
+ else
+ str = str .. " [" .. self.MaxArmor .. " armor]"
+ end
+ end
+
+ if ply.pac_healthbars_uidtotals then
+ if ply.pac_healthbars_uidtotals[self.UniqueID] then
+ if self.HealthBars == 1 then
+ str = str .. " [" .. ply.pac_healthbars_uidtotals[self.UniqueID] .. " / " .. self.BarsAmount .. " EX]"
+ elseif self.HealthBars >= 1 then
+ str = str .. " [" .. ply.pac_healthbars_uidtotals[self.UniqueID] .. " EX (" .. (self.healthbar_index or "0") .. " / " .. self.HealthBars .. ")]"
+ end
+ end
+ end
+ return str
+end
+
+function PART:SendModifier(str)
+ --pac.healthmod_part_UID_caches[string.sub(self.UniqueID,1,8)] = self
+ register_UID(self, string.sub(self.UniqueID,1,8), self:GetPlayerOwner())
+
+ if self:IsHidden() then return end
+ if LocalPlayer() ~= self:GetPlayerOwner() then return end
+ if not GetConVar("pac_sv_health_modifier"):GetBool() then return end
+ if util.NetworkStringToID( "pac_request_healthmod" ) == 0 then self:SetError("This part is deactivated on the server") return end
+ pac.Blocked_Combat_Parts = pac.Blocked_Combat_Parts or {}
+ if pac.Blocked_Combat_Parts then
+ if pac.Blocked_Combat_Parts[self.ClassName] then return end
+ end
+ if not GetConVar("pac_sv_combat_enforce_netrate_monitor_serverside"):GetBool() then
+ if not pac.CountNetMessage() then self:SetInfo("Went beyond the allowance") return end
+ end
+ --pac.healthmod_part_UID_caches[self.UniqueID] = self
+ register_UID(self, self.UniqueID, self:GetPlayerOwner())
+ if self.Name ~= "" then pac.healthmod_part_UID_caches[self.Name] = self end
+ register_UID(self, self.Name, self:GetPlayerOwner())
+
+ if str == "MaxHealth" and self.ChangeHealth then
+ net.Start("pac_request_healthmod")
+ net.WriteString(self.UniqueID)
+ net.WriteString(self.ModifierId)
+ net.WriteString("MaxHealth")
+ net.WriteUInt(self.MaxHealth, 32)
+ net.WriteBool(self.FollowHealth)
+ net.SendToServer()
+ elseif str == "MaxArmor" and self.ChangeArmor then
+ net.Start("pac_request_healthmod")
+ net.WriteString(self.UniqueID)
+ net.WriteString(self.ModifierId)
+ net.WriteString("MaxArmor")
+ net.WriteUInt(self.MaxArmor, 32)
+ net.WriteBool(self.FollowArmor)
+ net.SendToServer()
+ elseif str == "DamageMultiplier" then
+ net.Start("pac_request_healthmod")
+ net.WriteString(self.UniqueID)
+ net.WriteString(self.ModifierId)
+ net.WriteString("DamageMultiplier")
+ net.WriteFloat(self.DamageMultiplier)
+ net.WriteBool(true)
+ net.SendToServer()
+ elseif str == "HealthBars" then
+ net.Start("pac_request_healthmod")
+ net.WriteString(self.UniqueID)
+ net.WriteString(self.ModifierId)
+ net.WriteString("HealthBars")
+ net.WriteUInt(self.HealthBars, 32)
+ net.WriteUInt(self.BarsAmount, 32)
+ net.WriteUInt(self.BarsLayer, 4)
+ net.WriteFloat(self.AbsorbFactor)
+ net.WriteBool(self.FollowHealthBars)
+ net.WriteBool(self.CountedHits)
+ net.WriteBool(self.NoOverflow)
+ net.SendToServer()
+
+ elseif str == "all" then
+ self:SendModifier("MaxHealth")
+ self:SendModifier("MaxArmor")
+ self:SendModifier("DamageMultiplier")
+ self:SendModifier("HealthBars")
+ end
+end
+
+function PART:SetHealthBars(val)
+ self.HealthBars = val
+ if pac.LocalPlayer ~= self:GetPlayerOwner() then return end
+ self:SendModifier("HealthBars")
+end
+
+function PART:SetBarsAmount(val)
+ self.BarsAmount = val
+ if pac.LocalPlayer ~= self:GetPlayerOwner() then return end
+ self:SendModifier("HealthBars")
+ self:UpdateHPBars()
+end
+
+function PART:SetBarsLayer(val)
+ self.BarsLayer = val
+ if pac.LocalPlayer ~= self:GetPlayerOwner() then return end
+ self:SendModifier("HealthBars")
+ self:UpdateHPBars()
+end
+
+function PART:SetAbsorbFactor(val)
+ self.AbsorbFactor = val
+ if pac.LocalPlayer ~= self:GetPlayerOwner() then return end
+ self:SendModifier("HealthBars")
+end
+
+function PART:SetMaxHealth(val)
+ self.MaxHealth = val
+ if pac.LocalPlayer ~= self:GetPlayerOwner() then return end
+ self:SendModifier("MaxHealth")
+end
+
+function PART:SetMaxArmor(val)
+ self.MaxArmor = val
+ if pac.LocalPlayer ~= self:GetPlayerOwner() then return end
+ self:SendModifier("MaxArmor")
+end
+
+function PART:SetDamageMultiplier(val)
+ self.DamageMultiplier = val
+ if pac.LocalPlayer ~= self:GetPlayerOwner() then return end
+ self:SendModifier("DamageMultiplier")
+ local sv_min = GetConVar("pac_sv_health_modifier_min_damagescaling"):GetInt()
+ if self.DamageMultiplier < sv_min then
+ self:SetInfo("Your damage scaling is beyond the server's minimum permitted! Server minimum is " .. sv_min)
+ else
+ self:SetInfo(nil)
+ end
+end
+
+function PART:OnRemove()
+ --pac.healthmod_part_UID_caches = {} --we'll need this part removed from the cache
+ register_UID(nil, string.sub(self.UniqueID,1,8), self:GetPlayerOwner())
+ register_UID(nil, self.UniqueID, self:GetPlayerOwner())
+ register_UID(nil, self.Name, self:GetPlayerOwner())
+
+ if pac.LocalPlayer ~= self:GetPlayerOwner() then return end
+ if util.NetworkStringToID( "pac_request_healthmod" ) == 0 then return end
+ local found_remaining_healthmod = false
+ for _,part in pairs(pac.GetLocalParts()) do
+ if part.ClassName == "health_modifier" and part ~= self then
+ found_remaining_healthmod = true
+ end
+ end
+ net.Start("pac_request_healthmod")
+ net.WriteString(self.UniqueID)
+ net.WriteString(self.ModifierId)
+ net.WriteString("OnRemove")
+ net.WriteFloat(0)
+ net.WriteBool(true)
+ net.SendToServer()
+
+ if not found_remaining_healthmod then
+ net.Start("pac_request_healthmod")
+ net.WriteString(self.UniqueID)
+ net.WriteString(self.ModifierId)
+ net.WriteString("MaxHealth")
+ net.WriteUInt(100,32)
+ net.WriteBool(true)
+ net.SendToServer()
+
+ net.Start("pac_request_healthmod")
+ net.WriteString(self.UniqueID)
+ net.WriteString(self.ModifierId)
+ net.WriteString("MaxArmor")
+ net.WriteUInt(100,32)
+ net.WriteBool(false)
+ net.SendToServer()
+ end
+
+ hook.Remove("HUDPaint", "extrahealth_total")
+ hook.Remove("HUDPaint", "extrahealth_"..self.UniqueID)
+ hook.Remove("HUDPaint", "extrahealth_layer_"..self.BarsLayer)
+end
+
+function PART:OnShow()
+ if self.ActivateOnShow then self:SendModifier("all") end
+end
+function PART:OnWorn()
+ if self.ActivateOnWear then self:SendModifier("all") end
+end
+
+function PART:OnHide()
+ if util.NetworkStringToID( "pac_request_healthmod" ) == 0 then self:SetError("This part is deactivated on the server") return end
+ if self.HPBarsResetOnHide then
+ net.Start("pac_request_healthmod")
+ net.WriteString(self.UniqueID)
+ net.WriteString(self.ModifierId)
+ net.WriteString("HealthBars")
+ net.WriteUInt(0, 32)
+ net.WriteUInt(0, 32)
+ net.WriteUInt(self.BarsLayer, 4)
+ net.WriteFloat(1)
+ net.WriteBool(self.FollowHealthBars)
+ net.SendToServer()
+ end
+ if self.MultiplierResetOnHide then
+ net.Start("pac_request_healthmod")
+ net.WriteString(self.UniqueID)
+ net.WriteString(self.ModifierId)
+ net.WriteString("DamageMultiplier")
+ net.WriteFloat(1)
+ net.WriteBool(true)
+ net.SendToServer()
+ end
+end
+
+function PART:Initialize()
+ self.healthbar_index = 0
+ --pac.healthmod_part_UID_caches[string.sub(self.UniqueID,1,8)] = self
+ register_UID(nil, string.sub(self.UniqueID,1,8), self:GetPlayerOwner())
+ if not GetConVar("pac_sv_health_modifier"):GetBool() or pac.Blocked_Combat_Parts[self.ClassName] then self:SetError("health modifiers are disabled on this server!") end
+end
+
+function PART:UpdateHPBars()
+ local ent = self:GetPlayerOwner()
+ if ent.pac_healthbars_uidtotals and ent.pac_healthbars_uidtotals[self.UniqueID] then
+ self.healthbar_index = math.ceil(ent.pac_healthbars_uidtotals[self.UniqueID] / self.BarsAmount)
+ if ent.pac_healthbars_uidtotals[self.UniqueID] then
+ self:SetInfo("Extra healthbars:\nHP is " .. ent.pac_healthbars_uidtotals[self.UniqueID] .. "/" .. self.HealthBars * self.BarsAmount .. "\n" .. self.healthbar_index .. " of " .. self.HealthBars .. " bars")
+ end
+ end
+end
+
+--expected structure : pac_healthbars uid_or_name action number
+--actions: set, add, subtract, refill, replenish, remove
+concommand.Add("pac_healthbar", function(ply, cmd, args)
+ local uid_or_name = args[1]
+ local num = tonumber(args[3]) or 0
+ pac.healthmod_part_UID_caches[ply] = pac.healthmod_part_UID_caches[ply] or {}
+ if pac.healthmod_part_UID_caches[ply][uid_or_name] ~= nil and args[2] ~= nil then
+ local part = pac.healthmod_part_UID_caches[ply][uid_or_name]
+ uid = part.UniqueID
+ local action = args[2] or ""
+
+ --doesnt make sense to add or subtract 0
+ if ((action == "add" or action == "subtract") and num == 0) or (action == "") then return end
+ --replenish means set to full
+ if action == "refill" or action == "replenish" then
+ action = "set"
+ num = part.BarsAmount * part.HealthBars
+ end
+ if action == "remove" then action = "set" num = 0 end
+ net.Start("pac_request_extrahealthbars_action")
+ net.WriteString(uid)
+ net.WriteString(action)
+ net.WriteInt(num, 16)
+ net.SendToServer()
+ end
+ if args[2] == nil then ply:PrintMessage(HUD_PRINTCONSOLE, "\nthis command needs at least two arguments.\nuid or name: the unique ID or the name of the part\naction: add, subtract, refill, replenish, remove, set\nnumber\n\nexample: pac_healthbar my_healthmod add 50\n") end
+end, nil, "changes your health modifier's extra health value. arguments:\nuid or name: the unique ID or the name of the part\naction: add, subtract, refill, replenish, remove, set\nnumber\n\nexample: pac_healthbar my_healthmod add 50")
+
+BUILDER:Register()
\ No newline at end of file
diff --git a/lua/pac3/core/client/parts/hitscan.lua b/lua/pac3/core/client/parts/hitscan.lua
new file mode 100644
index 000000000..111dc91f0
--- /dev/null
+++ b/lua/pac3/core/client/parts/hitscan.lua
@@ -0,0 +1,260 @@
+language.Add("pac_hitscan", "Hitscan")
+--local vector_origin = vector_origin
+--local angle_origin = Angle(0,0,0)
+--local WorldToLocal = WorldToLocal
+
+local BUILDER, PART = pac.PartTemplate("base_drawable")
+
+PART.ClassName = "hitscan"
+PART.Group = "combat"
+PART.Icon = "icon16/user_gray.png"
+
+PART.ImplementsDoubleClickSpecified = true
+
+BUILDER:StartStorableVars()
+ :GetSet("ServerBullets", true, {description = "serverside bullets can do damage and exert a physical impact force"})
+ :SetPropertyGroup("bullet properties")
+ :GetSet("BulletImpact", false)
+ :GetSet("Damage", 1, {editor_onchange = function (self,val) return math.floor(math.Clamp(val,0,268435455)) end})
+ :GetSet("Force",1000, {editor_onchange = function (self,val) return math.floor(math.Clamp(val,0,65535)) end})
+ :GetSet("AffectSelf", false, {description = "whether to allow to damage yourself"})
+ :GetSet("DamageFalloff", false, {description = "enable damage falloff. The lowest damage is not a fixed damage number, but a fraction of the total initial damage.\nThe server can still restrict the maximum distance of all bullets"})
+ :GetSet("DamageFalloffDistance", 5000, {editor_onchange = function (self,val) return math.floor(math.Clamp(val,0,65535)) end})
+ :GetSet("DamageFalloffFraction", 0.5, {editor_clamp = {0,1}})
+ :GetSet("DamageType", "generic", {enums = {
+ generic = 0, --generic damage
+ crush = 1, --caused by physics interaction
+ bullet = 2, --bullet damage
+ slash = 4, --sharp objects, such as manhacks or other npcs attacks
+ burn = 8, --damage from fire
+ vehicle = 16, --hit by a vehicle
+ fall = 32, --fall damage
+ blast = 64, --explosion damage
+ club = 128, --crowbar damage
+ shock = 256, --electrical damage, shows smoke at the damage position
+ sonic = 512, --sonic damage,used by the gargantua and houndeye npcs
+ energybeam = 1024, --laser
+ nevergib = 4096, --don't create gibs
+ alwaysgib = 8192, --always create gibs
+ drown = 16384, --drown damage
+ paralyze = 32768, --same as dmg_poison
+ nervegas = 65536, --neurotoxin damage
+ poison = 131072, --poison damage
+ acid = 1048576, --
+ airboat = 33554432, --airboat gun damage
+ blast_surface = 134217728, --this won't hurt the player underwater
+ buckshot = 536870912, --the pellets fired from a shotgun
+ direct = 268435456, --
+ dissolve = 67108864, --forces the entity to dissolve on death
+ drownrecover = 524288, --damage applied to the player to restore health after drowning
+ physgun = 8388608, --damage done by the gravity gun
+ plasma = 16777216, --
+ prevent_physics_force = 2048, --
+ radiation = 262144, --radiation
+ removenoragdoll = 4194304, --don't create a ragdoll on death
+ slowburn = 2097152, --
+
+ explosion = -1, -- util.BlastDamage
+ fire = -1, -- ent:Ignite(5)
+
+ -- env_entity_dissolver
+ dissolve_energy = 0,
+ dissolve_heavy_electrical = 1,
+ dissolve_light_electrical = 2,
+ dissolve_core_effect = 3,
+
+ heal = -1,
+ armor = -1,
+ }
+ })
+ :GetSet("Spread", 0)
+ :GetSet("SpreadX", 1)
+ :GetSet("SpreadY", 1)
+ :GetSet("NumberBullets", 1, {editor_onchange = function (self,val) return math.floor(math.Clamp(val,0,511)) end})
+ :GetSet("DistributeDamage", false, {description = "whether or not the damage should be divided equally to all bullets in NumberBullets.\nThe server can still force multi-shots to do that"})
+ :GetSet("TracerSparseness", 1, {editor_onchange = function (self,val) return math.floor(math.Clamp(val,0,255)) end})
+ :GetSet("MaxDistance", 10000, {editor_onchange = function (self,val) return math.floor(math.Clamp(val,0,65535)) end})
+ :GetSet("TracerName", "Tracer", {enums = {
+ ["Default bullet tracer"] = "Tracer",
+ ["AR2 pulse-rifle tracer"] = "AR2Tracer",
+ ["Helicopter tracer"] = "HelicopterTracer",
+ ["Airboat gun tracer"] = "AirboatGunTracer",
+ ["Airboat gun heavy tracer"] = "AirboatGunHeavyTracer",
+ ["Gauss tracer"] = "GaussTracer",
+ ["Hunter tracer"] = "HunterTracer",
+ ["Strider tracer"] = "StriderTracer",
+ ["Gunship tracer"] = "GunshipTracer",
+ ["Toolgun tracer"] = "ToolTracer",
+ ["Laser tracer"] = "LaserTracer"
+ }})
+
+BUILDER:EndStorableVars()
+
+function PART:Initialize()
+ self.bulletinfo = {}
+ if not GetConVar("pac_sv_hitscan"):GetBool() or pac.Blocked_Combat_Parts[self.ClassName] then self:SetError("hitscan parts are disabled on this server!") end
+end
+
+function PART:OnShow()
+ self:Shoot()
+end
+
+function PART:OnDraw()
+ self:GetWorldPosition()
+ self:GetWorldAngles()
+end
+
+function PART:Shoot()
+ if self.NumberBullets == 0 then return end
+ if self.ServerBullets and self.Damage ~= 0 then
+ self:SendNetMessage()
+ else
+ self.bulletinfo.Attacker = self:GetRootPart():GetOwner()
+ self.ent = self:GetRootPart():GetOwner()
+ if self.Damage ~= 0 then self.bulletinfo.Damage = self.Damage end
+
+ self.bulletinfo.Src = self:GetWorldPosition()
+ self.bulletinfo.Dir = self:GetWorldAngles():Forward()
+ self.bulletinfo.Spread = Vector(self.SpreadX*self.Spread,self.SpreadY*self.Spread,0)
+
+ self.bulletinfo.Force = self.Force
+ self.bulletinfo.Distance = self.MaxDistance
+ self.bulletinfo.Num = self.NumberBullets
+ self.bulletinfo.Tracer = self.TracerSparseness --tracer every x bullets
+ self.bulletinfo.TracerName = self.TracerName
+ self.bulletinfo.DistributeDamage = self.DistributeDamage
+
+ self.bulletinfo.DamageFalloff = self.DamageFalloff
+ self.bulletinfo.DamageFalloffDistance = self.DamageFalloffDistance
+ self.bulletinfo.DamageFalloffFraction = self.DamageFalloffFraction
+
+ if IsValid(self.ent) then self.ent:FireBullets(self.bulletinfo) end
+ end
+end
+
+function PART:OnDoubleClickSpecified()
+ self:Shoot()
+end
+
+
+--NOT THE ACTUAL DAMAGE TYPES. UNIQUE IDS TO COMPRESS NET MESSAGES
+local damage_ids = {
+ generic = 0, --generic damage
+ crush = 1, --caused by physics interaction
+ bullet = 2, --bullet damage
+ slash = 3, --sharp objects, such as manhacks or other npcs attacks
+ burn = 4, --damage from fire
+ vehicle = 5, --hit by a vehicle
+ fall = 6, --fall damage
+ blast = 7, --explosion damage
+ club = 8, --crowbar damage
+ shock = 9, --electrical damage, shows smoke at the damage position
+ sonic = 10, --sonic damage,used by the gargantua and houndeye npcs
+ energybeam = 11, --laser
+ nevergib = 12, --don't create gibs
+ alwaysgib = 13, --always create gibs
+ drown = 14, --drown damage
+ paralyze = 15, --same as dmg_poison
+ nervegas = 16, --neurotoxin damage
+ poison = 17, --poison damage
+ acid = 18, --
+ airboat = 19, --airboat gun damage
+ blast_surface = 20, --this won't hurt the player underwater
+ buckshot = 21, --the pellets fired from a shotgun
+ direct = 22, --
+ dissolve = 23, --forces the entity to dissolve on death
+ drownrecover = 24, --damage applied to the player to restore health after drowning
+ physgun = 25, --damage done by the gravity gun
+ plasma = 26, --
+ prevent_physics_force = 27, --
+ radiation = 28, --radiation
+ removenoragdoll = 29, --don't create a ragdoll on death
+ slowburn = 30, --
+
+ fire = 31, -- ent:Ignite(5)
+
+ -- env_entity_dissolver
+ dissolve_energy = 32,
+ dissolve_heavy_electrical = 33,
+ dissolve_light_electrical = 34,
+ dissolve_core_effect = 35,
+
+ heal = 36,
+ armor = 37,
+}
+
+local tracer_ids = {
+ ["Tracer"] = 1,
+ ["AR2Tracer"] = 2,
+ ["HelicopterTracer"] = 3,
+ ["AirboatGunTracer"] = 4,
+ ["AirboatGunHeavyTracer"] = 5,
+ ["GaussTracer"] = 6,
+ ["HunterTracer"] = 7,
+ ["StriderTracer"] = 8,
+ ["GunshipTracer"] = 9,
+ ["ToolTracer"] = 10,
+ ["LaserTracer"] = 11
+}
+
+
+function PART:SendNetMessage()
+ if pac.LocalPlayer ~= self:GetPlayerOwner() then return end
+ if not GetConVar('pac_sv_hitscan'):GetBool() then return end
+ if util.NetworkStringToID( "pac_hitscan" ) == 0 then self:SetError("This part is deactivated on the server") return end
+ pac.Blocked_Combat_Parts = pac.Blocked_Combat_Parts or {}
+ if pac.Blocked_Combat_Parts[self.ClassName] then
+ return
+ end
+ if not GetConVar("pac_sv_combat_enforce_netrate_monitor_serverside"):GetBool() then
+ if not pac.CountNetMessage() then self:SetInfo("Went beyond the allowance") return end
+ end
+
+ net.Start("pac_hitscan", true)
+ net.WriteBool(self.AffectSelf)
+ net.WriteVector(self:GetWorldPosition())
+ net.WriteAngle(self:GetWorldAngles())
+
+ net.WriteUInt(damage_ids[self.DamageType] or 0,7)
+ net.WriteUInt(math.abs(math.Clamp(10000 * self.SpreadX*self.Spread, 0, 1048575)), 20)
+ net.WriteUInt(math.abs(math.Clamp(10000 * self.SpreadY*self.Spread, 0, 1048575)), 20)
+ net.WriteUInt(self.Damage, 28)
+ net.WriteUInt(self.TracerSparseness, 8)
+ net.WriteUInt(self.Force, 16)
+ net.WriteUInt(self.MaxDistance, 16)
+ net.WriteUInt(self.NumberBullets, 9)
+ net.WriteUInt(tracer_ids[self.TracerName] or 0, 4)
+ net.WriteBool(self.DistributeDamage)
+
+ net.WriteBool(self.DamageFalloff)
+ net.WriteUInt(self.DamageFalloffDistance, 16)
+ net.WriteUInt(math.Clamp(math.floor(self.DamageFalloffFraction * 1000),0, 1000), 10)
+
+ net.WriteString(string.sub(self.UniqueID,1,8))
+
+ net.SendToServer()
+end
+
+function PART:SetDamage(val)
+ self.Damage = val
+ local sv_max = GetConVar("pac_sv_hitscan_max_damage"):GetInt()
+ if self.Damage > sv_max then
+ self:SetInfo("Your damage is beyond the server's maximum permitted! Server max is " .. sv_max)
+ else
+ self:SetInfo(nil)
+ end
+end
+
+function PART:SetNumberBullets(val)
+ self.NumberBullets = val
+ local sv_max = GetConVar("pac_sv_hitscan_max_bullets"):GetInt()
+ if self.NumberBullets > sv_max then
+ self:SetInfo("Your bullet count is beyond the server's maximum permitted! Server max is " .. sv_max)
+ else
+ self:SetInfo(nil)
+ end
+end
+
+
+BUILDER:Register()
+
diff --git a/lua/pac3/core/client/parts/interpolated_multibone.lua b/lua/pac3/core/client/parts/interpolated_multibone.lua
new file mode 100644
index 000000000..168709755
--- /dev/null
+++ b/lua/pac3/core/client/parts/interpolated_multibone.lua
@@ -0,0 +1,273 @@
+local BUILDER, PART = pac.PartTemplate("base_drawable")
+
+PART.ClassName = "interpolated_multibone"
+PART.FriendlyName = "interpolator"
+PART.Group = 'advanced'
+PART.Icon = 'icon16/table_multiple.png'
+PART.is_model_part = false
+
+PART.ManualDraw = true
+PART.HandleModifiersManually = false
+
+BUILDER:StartStorableVars()
+ :SetPropertyGroup("test")
+ :GetSet("Preview", false)
+ :SetPropertyGroup("Interpolation")
+ :GetSet("LerpValue",0)
+ :GetSet("Power",1)
+ :GetSet("InterpolatePosition", true)
+ :GetSet("InterpolateAngles", true)
+ :SetPropertyGroup("Nodes")
+ :GetSetPart("Node1", {editor_friendly = "part 1"})
+ :GetSetPart("Node2", {editor_friendly = "part 2"})
+ :GetSetPart("Node3", {editor_friendly = "part 3"})
+ :GetSetPart("Node4", {editor_friendly = "part 4"})
+ :GetSetPart("Node5", {editor_friendly = "part 5"})
+ :GetSetPart("Node6", {editor_friendly = "part 6"})
+ :GetSetPart("Node7", {editor_friendly = "part 7"})
+ :GetSetPart("Node8", {editor_friendly = "part 8"})
+ :GetSetPart("Node9", {editor_friendly = "part 9"})
+ :GetSetPart("Node10", {editor_friendly = "part 10"})
+ :GetSetPart("Node11", {editor_friendly = "part 11"})
+ :GetSetPart("Node12", {editor_friendly = "part 12"})
+ :GetSetPart("Node13", {editor_friendly = "part 13"})
+ :GetSetPart("Node14", {editor_friendly = "part 14"})
+ :GetSetPart("Node15", {editor_friendly = "part 15"})
+ :GetSetPart("Node16", {editor_friendly = "part 16"})
+ :GetSetPart("Node17", {editor_friendly = "part 17"})
+ :GetSetPart("Node18", {editor_friendly = "part 18"})
+ :GetSetPart("Node19", {editor_friendly = "part 19"})
+ :GetSetPart("Node20", {editor_friendly = "part 20"})
+:EndStorableVars()
+
+function PART:OnRemove()
+ SafeRemoveEntityDelayed(self.Owner,0.1)
+end
+
+function PART:GetNiceName()
+ if self.Name ~= "" then return self.Name end
+
+ if not self.valid_nodes then return self.FriendlyName end
+ local has_valid_node = false
+ for i,b in ipairs(self.valid_nodes) do
+ if b then has_valid_node = true end
+ end
+ if not has_valid_node then return self.FriendlyName end
+
+ local str = "Interpolator: "
+ local firstnodecounted = false
+ for i=1,20,1 do
+ if IsValid(self["Node"..i]) then
+ str = str .. (firstnodecounted and "; " or "") .. "[" .. i .. "]" .. (self["Node"..i].Name ~= "" and self["Node"..i].Name or self["Node"..i].ClassName)
+ firstnodecounted = true
+ end
+ end
+ return str
+end
+
+function PART:Initialize()
+ self.nodes = {}
+ self.valid_nodes = {}
+ self.Owner = pac.CreateEntity("models/pac/default.mdl")
+ self.Owner:SetNoDraw(true)
+ self.valid_time = CurTime() + 1
+end
+
+function PART:OnShow()
+ self.valid_time = CurTime()
+end
+
+function PART:OnHide()
+ pac.RemoveHook("PostDrawOpaqueRenderables", "Multibone_draw"..self.UniqueID)
+
+end
+
+function PART:OnRemove()
+ pac.RemoveHook("PostDrawOpaqueRenderables", "Multibone_draw"..self.UniqueID)
+end
+--NODES self 1 2 3
+--STAGE 0 1 2 3
+--PROPORTION 0 0.5 0 0.5 0 0.5 3
+function PART:OnDraw()
+ self:UpdateNodes()
+ if self.valid_time > CurTime() then return end
+
+ self.pos = self.pos or self:GetWorldPosition()
+ self.ang = self.ang or self:GetWorldAngles()
+
+ if not self.Preview then pac.RemoveHook("PostDrawOpaqueRenderables", "Multibone_draw"..self.UniqueID) end
+
+ local stage = math.max(0,math.floor(self.LerpValue))
+ local proportion = math.max(0,self.LerpValue) % 1
+
+ if self.Preview then
+ pac.AddHook("PostDrawOpaqueRenderables", "Multibone_draw"..self.UniqueID, function()
+ render.DrawLine(self.pos,self.pos + self.ang:Forward()*50, Color(255,0,0))
+ render.DrawLine(self.pos,self.pos - self.ang:Right()*50, Color(0,255,0))
+ render.DrawLine(self.pos,self.pos + self.ang:Up()*50, Color(0,0,255))
+ render.DrawWireframeSphere(self.pos, 8 + 2*math.sin(5*RealTime()), 15, 15, Color(255,255,255), true)
+ local origin_pos = self:GetWorldPosition():ToScreen()
+ draw.DrawText("0 origin", "DermaDefaultBold", origin_pos.x, origin_pos.y)
+
+ for i=1,20,1 do
+
+ if i == 1 and self.valid_nodes[i] then
+ local startpos = self:GetWorldPosition()
+ local endpos = self.nodes["Node"..i]:GetWorldPosition()
+ local endang = self.nodes["Node"..i]:GetWorldAngles()
+ local screen_endpos = endpos:ToScreen()
+ render.DrawLine(endpos,endpos + endang:Forward()*4, Color(255,0,0))
+ render.DrawLine(endpos,endpos - endang:Right()*4, Color(0,255,0))
+ render.DrawLine(endpos,endpos + endang:Up()*4, Color(0,0,255))
+ render.DrawLine(self:GetWorldPosition(),self.nodes["Node"..i]:GetWorldPosition(), Color(255,255,255))
+ elseif self.valid_nodes[i - 1] and self.valid_nodes[i] then
+ local startpos = self.nodes["Node"..i-1]:GetWorldPosition()
+ local endpos = self.nodes["Node"..i]:GetWorldPosition()
+ local endang = self.nodes["Node"..i]:GetWorldAngles()
+ local screen_endpos = endpos:ToScreen()
+ render.DrawLine(endpos,endpos + endang:Forward()*4, Color(255,0,0))
+ render.DrawLine(endpos,endpos - endang:Right()*4, Color(0,255,0))
+ render.DrawLine(endpos,endpos + endang:Up()*4, Color(0,0,255))
+ render.DrawLine(self.nodes["Node"..i-1]:GetWorldPosition(),self.nodes["Node"..i]:GetWorldPosition(), Color(255,255,255))
+ end
+
+ end
+ end)
+ end
+ self:Interpolate(stage,proportion)
+
+end
+
+function PART:UpdateNodes()
+ for i=1,20,1 do
+ self.nodes["Node"..i] = self["Node"..i]
+ self.valid_nodes[i] = IsValid(self["Node"..i]) and self["Node"..i].GetWorldPosition
+ end
+end
+
+function PART:SetWorldPos(x,y,z)
+ self.pos.x = x
+ self.pos.y = y
+ self.pos.z = z
+end
+
+function PART:Interpolate(stage, proportion)
+ --print("Calculated the stage. We are at stage " .. stage .. " between nodes " .. stage .. " and " .. (stage + 1))
+ local firstnode
+ if stage <= 0 then
+ firstnode = self
+ else
+ firstnode = self.nodes["Node"..stage] or self
+ end
+
+
+ local secondnode = self.nodes["Node"..stage+1]
+ if firstnode == nil or firstnode == NULL or not firstnode.GetWorldPosition then firstnode = self end
+ if secondnode == nil or secondnode == NULL or not secondnode.GetWorldPosition then secondnode = self end
+
+ proportion = math.pow(proportion,self.Power)
+ if secondnode ~= nil and secondnode ~= NULL then
+ if self.InterpolatePosition then
+ self.pos = LerpVector(proportion,firstnode:GetWorldPosition(), secondnode:GetWorldPosition())
+ --self.pos = (1-proportion)*self:GetWorldPosition() + (self:GetWorldPosition())*proportion
+ else self.pos = self:GetWorldPosition() end
+ if self.InterpolateAngles then
+ self.ang = LerpAngle(proportion, firstnode:GetWorldAngles(), secondnode:GetWorldAngles())
+ --self.ang = GetClosestAngleMidpoint(self:GetWorldAngles(), self:GetWorldAngles(), proportion)
+ else self.ang = self:GetWorldAngles() end
+ --self.ang = (1-proportion)*(self:GetWorldAngles() + Angle(360,360,360)) + (self:GetWorldAngles() + Angle(360,360,360))*proportion
+ end
+
+ self.Owner:SetPos(self.pos)
+ self.Owner:SetAngles(self.ang)
+end
+
+function GetClosestAngleMidpoint(a1, a2, proportion)
+ --print(a1)
+ --print(a2)
+ local axes = {"p","y","r"}
+ local ang_delta_candidate1
+ local ang_delta_candidate2
+ local ang_delta_candidate3
+ local ang_delta_final
+ local final_ang = Angle()
+ for _,ax in pairs(axes) do
+ ang_delta_candidate1 = a2[ax] - a1[ax]
+ ang_delta_candidate2 = (a2[ax] + 360) - a1[ax]
+ ang_delta_candidate3 = (a2[ax] - 360) - a1[ax]
+ ang_delta_final = 180
+ if math.abs(ang_delta_candidate1) < math.abs(ang_delta_final) then
+ ang_delta_final = ang_delta_candidate1
+ end
+ if math.abs(ang_delta_candidate2) < math.abs(ang_delta_final) then
+ ang_delta_final = ang_delta_candidate2
+ end
+ if math.abs(ang_delta_candidate3) < math.abs(ang_delta_final) then
+ ang_delta_final = ang_delta_candidate3
+ end
+ --print("at "..ax.." 1:"..ang_delta_candidate1.." 2:"..ang_delta_candidate2.." 3:"..ang_delta_candidate3.." pick "..ang_delta_final)
+ final_ang[ax] = a1[ax] + proportion * ang_delta_final
+ end
+
+ return final_ang
+end
+
+function PART:GoTo(part)
+ self.pos = part:GetWorldPosition() or self:GetWorldPosition()
+ self.ang = part:GetWorldAngles() or self:GetWorldAngles()
+end
+
+--we need to know the stage and proportion (progress)
+--e.g. lerp 0.5 is stage 0, proportion 0.5 because it's 50% toward Node 1
+--e.g. lerp 2.2 is stage 2, proportion 0.2 because it's 20% toward Node 3
+function PART:GetInterpolationParameters()
+ stage = math.max(0,math.floor(self.LerpValue))
+ proportion = math.max(0,self.LerpValue) % 1
+ --print("Calculated the stage. We are at stage " .. stage .. " between nodes " .. stage .. " and " .. (stage + 1))
+ --print("proportion is " .. proportion)
+ return stage, proportion
+end
+
+function PART:GetNodeAngle(nodenumber)
+ --print("node" .. nodenumber .. " angle " .. self.__['Node'..nodenumber].Angles)
+ --print("node" .. nodenumber .. " world angle " .. self.__['Node'..nodenumber]:GetWorldAngles())
+
+ --return self.Node1:GetWorldAngles()
+end
+
+function PART:GetNodePosition(nodenumber)
+ --print("node" .. nodenumber .. " position " .. self.__['Node'..nodenumber].Position)
+ --print("node" .. nodenumber .. " world position " .. self.__['Node'..nodenumber]:GetWorldPosition())
+ --return self.Node1:GetWorldPosition()
+end
+
+function PART:InterpolateFromLerp(lerp)
+end
+
+function PART:InterpolateFromNodes(firstnode, secondnode, proportion)
+ --position_interpolated = InterpolateFromStage("position", stage, self.Lerp)
+end
+
+function PART:InterpolateFromStage(stage, proportion)
+ self:InterpolateFromNodes(stage, stage + 1)
+end
+
+function PART:InterpolateAngle()
+
+end
+
+
+function PART:SetInterpolatePosition(b)
+ --print(type(b).." "..b)
+ self.InterpolatePosition = b
+end
+
+function PART:SetInterpolateAngles(b)
+ --print(type(b).." "..b)
+ self.InterpolateAngles = b
+end
+
+
+
+
+BUILDER:Register()
diff --git a/lua/pac3/core/client/parts/jiggle.lua b/lua/pac3/core/client/parts/jiggle.lua
index ec5585a27..ef8b89899 100644
--- a/lua/pac3/core/client/parts/jiggle.lua
+++ b/lua/pac3/core/client/parts/jiggle.lua
@@ -12,6 +12,9 @@ PART.Group = 'model'
PART.Icon = 'icon16/chart_line.png'
BUILDER:StartStorableVars()
+
+ BUILDER:SetPropertyGroup("orientation")
+ BUILDER:SetPropertyGroup("dynamics")
BUILDER:GetSet("Strain", 0.5, {editor_onchange = function(self, num)
self.sens = 0.25
num = tonumber(num)
@@ -19,22 +22,27 @@ BUILDER:StartStorableVars()
end})
BUILDER:GetSet("Speed", 1)
BUILDER:GetSet("ConstantVelocity", Vector(0, 0, 0))
- BUILDER:GetSet("LocalVelocity", true)
- BUILDER:GetSet("JiggleAngle", true)
- BUILDER:GetSet("JigglePosition", true)
-
- BUILDER:GetSet("ConstrainPitch", false)
- BUILDER:GetSet("ConstrainYaw", false)
- BUILDER:GetSet("ConstrainRoll", false)
+ BUILDER:GetSet("InitialVelocity", Vector(0, 0, 0))
+ BUILDER:GetSet("LocalVelocity", true, {description = "Whether Constant Velocity and Initial Velocity should use the jiggle part's angles instead of being applied by world coordinates"})
+ BUILDER:GetSet("ResetOnHide", false, {description = "Reinitializes the container's position to the base part position, and its speeds to 0, when the part is shown."})
+ BUILDER:GetSet("Ground", false)
- BUILDER:GetSet("ConstrainX", false)
- BUILDER:GetSet("ConstrainY", false)
- BUILDER:GetSet("ConstrainZ", false)
+ BUILDER:SetPropertyGroup("angles")
+ BUILDER:GetSet("JiggleAngle", true)
+ BUILDER:GetSet("ClampAngles", false, {description = "Restrict the angles so it can't go beyond a certain amount too far from the jiggle's base angle. Components are below"})
+ BUILDER:GetSet("AngleClampAmount", Vector(180,180,180))
+ BUILDER:GetSet("ConstrainPitch", false, {description = "Do not jiggle the angles on the pitch component\nThe pitch that will remain is the jiggle's current or initial global pitch.\nThat only resets when the part is created, or if reset on hide, when shown; Or, it starts moving again when you turn this off."})
+ BUILDER:GetSet("ConstrainYaw", false, {description = "Do not jiggle the angles on the yaw component\nThe yaw that will remain is the jiggle's current or initial global yaw.\nThat only resets when the part is created, or if reset on hide, when shown; Or, it starts moving again when you turn this off."})
+ BUILDER:GetSet("ConstrainRoll", false, {description = "Do not jiggle the angles on the roll component\nThe roll that will remain is the jiggle's current or initial global roll.\nThat only resets when the part is created, or if reset on hide, when shown; Or, it starts moving again when you turn this off."})
+ BUILDER:SetPropertyGroup("position")
+ BUILDER:GetSet("JigglePosition", true)
BUILDER:GetSet("ConstrainSphere", 0)
BUILDER:GetSet("StopRadius", 0)
- BUILDER:GetSet("Ground", false)
- BUILDER:GetSet("ResetOnHide", false)
+ BUILDER:GetSet("ConstrainX", false, {description = "Do not jiggle on X position coordinates"})
+ BUILDER:GetSet("ConstrainY", false, {description = "Do not jiggle on Y position coordinates"})
+ BUILDER:GetSet("ConstrainZ", false, {description = "Do not jiggle on Z position coordinates"})
+
BUILDER:EndStorableVars()
local math_AngleDifference = math.AngleDifference
@@ -63,6 +71,15 @@ function PART:OnShow()
if self.ResetOnHide then
self:Reset()
end
+
+ if not self.InitialVelocity then return end
+ local ang = self:GetWorldAngles()
+ if self.LocalVelocity then
+ self.vel = self.InitialVelocity.x * ang:Forward() + self.InitialVelocity.y * ang:Right() + self.InitialVelocity.z * ang:Up()
+ else
+ self.vel = self.InitialVelocity
+ end
+
end
local inf, ninf = math.huge, -math.huge
@@ -89,7 +106,7 @@ function PART:OnDraw()
self.vel = self.vel or VectorRand()
self.pos = self.pos or pos * 1
- if self.StopRadius ~= 0 and self.pos and self.pos:Distance(pos) < self.StopRadius then
+ if self.StopRadius ~= 0 and self.pos and self.pos:DistToSqr(pos) < (self.StopRadius * self.StopRadius) then
self.vel = Vector()
return
end
@@ -160,18 +177,36 @@ function PART:OnDraw()
if not self.ConstrainPitch then
self.angvel.p = self.angvel.p + math_AngleDifference(ang.p, self.ang.p)
self.ang.p = math_AngleDifference(self.ang.p, self.angvel.p * -speed)
+ if self.ClampAngles then
+ local p_angdiff = math_AngleDifference(self.ang.p, ang.p)
+ if p_angdiff > self.AngleClampAmount.x or p_angdiff < -self.AngleClampAmount.x then
+ self.ang.p = ang.p + math.Clamp(p_angdiff,-self.AngleClampAmount.x,self.AngleClampAmount.x)
+ end
+ end
self.angvel.p = self.angvel.p * self.Strain
end
if not self.ConstrainYaw then
self.angvel.y = self.angvel.y + math_AngleDifference(ang.y, self.ang.y)
- self.ang.y = math_AngleDifference(self.ang.y, self.angvel.y * -speed)
+ self.ang.y = math_AngleDifference(self.ang.y, self.angvel.y * -speed)
+ if self.ClampAngles then
+ local y_angdiff = math_AngleDifference(self.ang.y, ang.y)
+ if y_angdiff > self.AngleClampAmount.y or y_angdiff < -self.AngleClampAmount.y then
+ self.ang.y = ang.y + math.Clamp(y_angdiff,-self.AngleClampAmount.y,self.AngleClampAmount.y)
+ end
+ end
self.angvel.y = self.angvel.y * self.Strain
end
if not self.ConstrainRoll then
self.angvel.r = self.angvel.r + math_AngleDifference(ang.r, self.ang.r)
- self.ang.r = math_AngleDifference(self.ang.r, self.angvel.r * -speed)
+ self.ang.r = math_AngleDifference(self.ang.r, self.angvel.r * -speed)
+ if self.ClampAngles then
+ local r_angdiff = math_AngleDifference(self.ang.r, ang.r)
+ if r_angdiff > self.AngleClampAmount.x or r_angdiff < -self.AngleClampAmount.z then
+ self.ang.r = ang.r + math.Clamp(r_angdiff,-self.AngleClampAmount.z,self.AngleClampAmount.z)
+ end
+ end
self.angvel.r = self.angvel.r * self.Strain
end
else
diff --git a/lua/pac3/core/client/parts/legacy/entity.lua b/lua/pac3/core/client/parts/legacy/entity.lua
index 5d3d240fc..f46778e9b 100644
--- a/lua/pac3/core/client/parts/legacy/entity.lua
+++ b/lua/pac3/core/client/parts/legacy/entity.lua
@@ -605,14 +605,15 @@ do
end
pac.AddHook("CreateMove", "legacy_entity_part_speed_modifier", function(cmd)
+ local plyTable = pac.LocalPlayer:GetTable()
if cmd:KeyDown(IN_SPEED) then
- mod_speed(cmd, pac.LocalPlayer.pac_sprint_speed)
+ mod_speed(cmd, plyTable.pac_sprint_speed)
elseif cmd:KeyDown(IN_WALK) then
- mod_speed(cmd, pac.LocalPlayer.pac_walk_speed)
+ mod_speed(cmd, plyTable.pac_walk_speed)
elseif cmd:KeyDown(IN_DUCK) then
- mod_speed(cmd, pac.LocalPlayer.pac_crouch_speed)
+ mod_speed(cmd, plyTable.pac_crouch_speed)
else
- mod_speed(cmd, pac.LocalPlayer.pac_run_speed)
+ mod_speed(cmd, plyTable.pac_run_speed)
end
end)
end
diff --git a/lua/pac3/core/client/parts/legacy/light.lua b/lua/pac3/core/client/parts/legacy/light.lua
index ba156c235..69a274866 100644
--- a/lua/pac3/core/client/parts/legacy/light.lua
+++ b/lua/pac3/core/client/parts/legacy/light.lua
@@ -22,7 +22,7 @@ local DynamicLight = DynamicLight
function PART:OnDraw()
local pos = self:GetDrawPosition()
- local light = self.light or DynamicLight(tonumber(self.UniqueID))
+ local light = self.light or DynamicLight(tonumber(string.sub(self:GetPrintUniqueID(),1,7), 16))
light.Pos = pos
@@ -50,4 +50,4 @@ function PART:OnHide()
end
end
-BUILDER:Register()
\ No newline at end of file
+BUILDER:Register()
diff --git a/lua/pac3/core/client/parts/legacy/model.lua b/lua/pac3/core/client/parts/legacy/model.lua
index 97a34dd91..e38ddc1a8 100644
--- a/lua/pac3/core/client/parts/legacy/model.lua
+++ b/lua/pac3/core/client/parts/legacy/model.lua
@@ -409,7 +409,7 @@ surface.CreateFont("pac_urlobj_loading",
-- ugh lol
local function RealDrawModel(self, ent, pos, ang)
if self.Mesh then
- ent:SetModelScale(0,0)
+ ent:SetModelScale(0.001, 0)
ent:DrawModel()
local matrix = Matrix()
@@ -662,13 +662,13 @@ do
self.Mesh = nil
local real_model = modelPath
- local ret = hook.Run("pac_model:SetModel", self, modelPath, self.ModelFallback)
+ local ret = pac.CallHook("model:SetModel", self, modelPath, self.ModelFallback)
if ret == nil then
- real_model = pac.FilterInvalidModel(real_model,self.ModelFallback)
+ real_model = pac.FilterInvalidModel(real_model, self.ModelFallback)
else
modelPath = ret or modelPath
real_model = modelPath
- real_model = pac.FilterInvalidModel(real_model,self.ModelFallback)
+ real_model = pac.FilterInvalidModel(real_model, self.ModelFallback)
end
self.Model = modelPath
@@ -682,7 +682,7 @@ do
end
end
-local NORMAL = Vector(1,1,1)
+local NORMAL = Vector(1, 1, 1)
function PART:CheckScale()
-- RenderMultiply doesn't work with this..
@@ -704,7 +704,7 @@ function PART:SetAlternativeScaling(b)
end
function PART:SetScale(var)
- var = var or Vector(1,1,1)
+ var = var or Vector(1, 1, 1)
self.Scale = var
diff --git a/lua/pac3/core/client/parts/legacy/ogg.lua b/lua/pac3/core/client/parts/legacy/ogg.lua
index db2d6c200..0ed146cce 100644
--- a/lua/pac3/core/client/parts/legacy/ogg.lua
+++ b/lua/pac3/core/client/parts/legacy/ogg.lua
@@ -208,7 +208,7 @@ function PART:PlaySound(_, additiveVolumeFraction)
self.last_stream = stream
end
-function PART:StopSound()
+function PART:StopSound(force_stop)
for key, stream in pairs(self.streams) do
if not stream:IsValid() then self.streams[key] = nil goto CONTINUE end
@@ -219,6 +219,7 @@ function PART:StopSound()
stream:Stop()
end
end
+ if force_stop then stream:Stop() end
::CONTINUE::
end
end
diff --git a/lua/pac3/core/client/parts/legacy/sound.lua b/lua/pac3/core/client/parts/legacy/sound.lua
index 8cd06b100..166f1bee0 100644
--- a/lua/pac3/core/client/parts/legacy/sound.lua
+++ b/lua/pac3/core/client/parts/legacy/sound.lua
@@ -8,6 +8,8 @@ PART.ThinkTime = 0
PART.Group = 'effects'
PART.Icon = 'icon16/sound.png'
+PART.ImplementsDoubleClickSpecified = true
+
BUILDER:StartStorableVars()
BUILDER:SetPropertyGroup("generic")
BUILDER:GetSet("Sound", "")
@@ -42,6 +44,9 @@ function PART:Initialize()
end
function PART:OnShow(from_rendering)
+ local pos = self:GetWorldPosition()
+ if pos:DistToSqr(pac.EyePos) > pac.sounds_draw_dist_sqr then return end
+
if not from_rendering then
self.played_overlapping = false
self:PlaySound()
@@ -80,10 +85,31 @@ function PART:OnHide()
end
end
+--this is not really stopping the sound, rather putting the volume very low so the sounds don't replay when going back in range
+function PART:Silence(b)
+ if not self.csptch then return end
+ if not self.csptch:IsPlaying() then return end
+
+ if b then
+ self.csptch:ChangeVolume(0.01, 0)
+ else
+ self.csptch:ChangeVolume(math.Clamp(self.Volume * pac.volume, 0.001, 1), 0)
+ end
+end
+
function PART:OnThink()
+ local pos = self:GetWorldPosition()
+ if pos:DistToSqr(pac.EyePos) > pac.sounds_draw_dist_sqr then
+ self.out_of_range = true
+ self:Silence(true)
+ else
+ if self.out_of_range then self:Silence(false) end
+ self.out_of_range = false
+ end
if not self.csptch then
self:PlaySound()
- else
+ end
+ if self.csptch then
if self.Loop then
pac.playing_sound = true
if not self.csptch:IsPlaying() then self.csptch:Play() end
@@ -148,7 +174,7 @@ function PART:SetVolume(num)
end
if self.csptch then
- self.csptch:ChangeVolume(math.Clamp(self.Volume, 0.001, 1), 0)
+ self.csptch:ChangeVolume(math.Clamp(self.Volume * pac.volume, 0.001, 1), 0)
end
end
@@ -244,6 +270,8 @@ function PART:PlaySound(osnd, ovol)
vol = self.Volume
end
+ vol = vol * pac.volume
+
local pitch
if self.MinPitch == self.MaxPitch then
@@ -277,10 +305,40 @@ function PART:PlaySound(osnd, ovol)
end
end
-function PART:StopSound()
+function PART:StopSound(force_stop)
if self.csptch then
self.csptch:Stop()
end
+ if force_stop and self.Overlapping then
+ local ent = self.RootOwner and self:GetRootPart():GetOwner() or self:GetOwner()
+ if IsValid(ent) then
+ local sounds = self.Sound:Split(";")
+ if string.match(sounds[1],"%[(%d-),(%d-)%]") then
+ local min, max = string.match(sounds[1],"%[(%d-),(%d-)%]")
+ if max < min then
+ max = min
+ end
+ for i=min, max, 1 do
+ snd = self.Sound:gsub("(%[%d-,%d-%])", i)
+ ent:StopSound(snd)
+ end
+ else
+ for i,snd in ipairs(sounds) do
+ ent:StopSound(snd)
+ end
+ end
+ end
+ end
+end
+
+function PART:OnDoubleClickSpecified()
+ if self.playing then
+ self:StopSound(true)
+ self.playing = false
+ else
+ self:PlaySound()
+ self.playing = true
+ end
end
local channels =
diff --git a/lua/pac3/core/client/parts/legacy/webaudio.lua b/lua/pac3/core/client/parts/legacy/webaudio.lua
index d278813d8..578fd4192 100644
--- a/lua/pac3/core/client/parts/legacy/webaudio.lua
+++ b/lua/pac3/core/client/parts/legacy/webaudio.lua
@@ -64,7 +64,7 @@ function PART:OnDraw()
local shouldMute = snd_mute_losefocus:GetBool()
local focus = system.HasFocus()
- local volume = shouldMute and not focus and 0 or self:GetVolume()
+ local volume = shouldMute and not focus and 0 or self:GetVolume() * pac.volume --we need to hack it into here because OnDraw is called often
for url, streamdata in pairs(self.streams) do
local stream = streamdata.stream
@@ -264,7 +264,7 @@ function PART:PlaySound()
self.last_stream = stream
end
-function PART:StopSound()
+function PART:StopSound(force_stop)
local toremove
for key, streamdata in pairs(self.streams) do
@@ -280,6 +280,7 @@ function PART:StopSound()
pcall(function() stream:SetTime(0) end)
stream:Pause()
end
+ if force_stop then stream:Stop() end
elseif stream then
toremove = toremove or {}
table.insert(toremove, key)
diff --git a/lua/pac3/core/client/parts/light.lua b/lua/pac3/core/client/parts/light.lua
index 36249ad3f..89fc0408f 100644
--- a/lua/pac3/core/client/parts/light.lua
+++ b/lua/pac3/core/client/parts/light.lua
@@ -18,11 +18,26 @@ BUILDER:StartStorableVars()
BUILDER:GetSet("Brightness", 8)
BUILDER:GetSet("Size", 100, {editor_sensitivity = 0.25})
BUILDER:GetSet("Color", Vector(1, 1, 1), {editor_panel = "color2"})
+ BUILDER:GetSet("Style", 0, {editor_clamp = {0, 12}, enums = {
+ ["Normal"] = "0",
+ ["Flicker A"] = "1",
+ ["Slow, strong pulse"] = "2",
+ ["Candle A"] = "3",
+ ["Fast strobe"] = "4",
+ ["Gentle pulse"] = "5",
+ ["Flicker B"] = "6",
+ ["Candle B"] = "7",
+ ["Candle C"] = "8",
+ ["Slow strobe"] = "9",
+ ["Fluorescent flicker"] = "10",
+ ["Slow pulse, noblack"] = "11",
+ ["Underwater light mutation"] = "12"
+ }})
BUILDER:EndStorableVars()
function PART:GetLight()
if not self.light then
- self.light = DynamicLight(tonumber(self:GetPrintUniqueID(), 16))
+ self.light = DynamicLight(tonumber(string.sub(self:GetPrintUniqueID(),1,7), 16))
end
self.light.decay = 0
self.light.dietime = math.huge
@@ -68,6 +83,10 @@ function PART:OnDraw()
self:GetLight().pos = pos
self:GetLight().dir = ang:Forward()
end
+function PART:SetStyle(val)
+ self.Style = val
+ self:GetLight().Style = self.Style
+end
function PART:SetSize(val)
self.Size = val
@@ -110,4 +129,4 @@ function PART:OnHide()
self:RemoveLight()
end
-BUILDER:Register()
\ No newline at end of file
+BUILDER:Register()
diff --git a/lua/pac3/core/client/parts/lock.lua b/lua/pac3/core/client/parts/lock.lua
new file mode 100644
index 000000000..2d428b208
--- /dev/null
+++ b/lua/pac3/core/client/parts/lock.lua
@@ -0,0 +1,601 @@
+local pac = pac
+local Vector = Vector
+local Angle = Angle
+local NULL = NULL
+local Matrix = Matrix
+
+local physics_point_ent_classes = {
+ ["prop_physics"] = true,
+ ["prop_physics_multiplayer"] = true,
+ ["prop_physics_respawnable"] = true,
+ ["prop_ragdoll"] = true,
+ ["weapon_striderbuster"] = true,
+ ["item_item_crate"] = true,
+ ["func_breakable_surf"] = true,
+ ["func_breakable"] = true,
+ ["physics_cannister"] = true,
+ ["npc_satchel"] = true,
+ ["npc_grenade_frag"] = true,
+}
+
+local convar_lock = GetConVar("pac_sv_lock")
+local convar_lock_grab = GetConVar("pac_sv_lock_grab")
+local convar_lock_max_grab_radius = GetConVar("pac_sv_lock_max_grab_radius")
+local convar_lock_teleport = GetConVar("pac_sv_lock_teleport")
+local convar_lock_aim = GetConVar("pac_sv_lock_aim")
+local convar_combat_enforce_netrate = GetConVar("pac_sv_combat_enforce_netrate_monitor_serverside")
+
+--sorcerous hack fix
+if convar_lock == nil then timer.Simple(10, function() convar_lock = GetConVar("pac_sv_lock") end) end
+if convar_lock_grab == nil then timer.Simple(10, function() convar_lock_grab = GetConVar("pac_sv_lock_grab") end) end
+if convar_lock_teleport == nil then timer.Simple(10, function() convar_lock_teleport = GetConVar("pac_sv_lock_teleport") end) end
+if convar_lock_aim == nil then timer.Simple(10, function() convar_lock_aim = GetConVar("pac_sv_lock_aim") end) end
+if convar_lock_max_grab_radius == nil then timer.Simple(10, function() convar_lock_max_grab_radius = GetConVar("pac_sv_lock_max_grab_radius") end) end
+if convar_combat_enforce_netrate == nil then timer.Simple(10, function() convar_combat_enforce_netrate = GetConVar("pac_sv_combat_enforce_netrate_monitor_serverside") end) end
+
+
+local BUILDER, PART = pac.PartTemplate("base_movable")
+
+PART.ClassName = "lock"
+PART.Group = "combat"
+PART.Icon = "icon16/lock.png"
+
+PART.ImplementsDoubleClickSpecified = true
+
+BUILDER:StartStorableVars()
+ :SetPropertyGroup("Behaviour")
+ :GetSet("Mode", "None", {enums = {
+ ["None"] = "None",
+ ["Grab"] = "Grab",
+ ["Teleport"] = "Teleport",
+ ["SetEyeang"] = "SetEyeang",
+ ["AimToPos"] = "AimToPos"
+ }})
+ :GetSet("OverrideAngles", true, {description = "Whether the part will rotate the entity alongside it, otherwise it changes just the position"})
+ :GetSet("RelativeGrab", false)
+ :GetSet("RestoreDelay", 1, {description = "Seconds until the entity's original angles before self.grabbing are re-applied"})
+ :GetSet("NoCollide", true, {description = "Whether to disable collisions on the entity while grabbed."})
+
+ :SetPropertyGroup("DetectionOrigin")
+ :GetSet("Radius", 20)
+ :GetSet("OffsetDownAmount", 0, {description = "Lowers the detect origin by some amount"})
+ :GetSetPart("TargetPart")
+ :GetSet("ContinuousSearch", false, {description = "Will search for entities until one is found. Otherwise only try once when part is shown."})
+ :GetSet("Preview", false)
+
+ :SetPropertyGroup("AimMode")
+ :GetSet("AffectPitch", true)
+ :GetSet("AffectYaw", true)
+ :GetSet("ContinuousAim", true)
+ :GetSet("SmoothAiming", false, {description = "Gradually ease into the target angle by only changing the angle by a fraction every frame instead of fully setting it immediately"})
+ :GetSet("SmoothFraction", 0.05, {editor_clamp = {0,1}})
+
+ :SetPropertyGroup("TeleportSafety")
+ :GetSet("ClampDistance", false, {description = "Prevents the teleport from going too far (By Radius amount). For example, if you use hitpos bone on a pac model, it can act as a safety in case the raycast falls out of bounds."})
+ :GetSet("SlopeSafety", false, {description = "Teleports a bit up in case you end up on a slope and get stuck."})
+
+ :SetPropertyGroup("PlayerCameraOverride")
+ :GetSet("OverrideEyeAngles", false, {description = "Whether the part will try to override players' eye angles. Requires OverrideAngles and user consent"})
+ :GetSet("OverrideEyePosition", false, {description = "Whether the part will try to override players' view position to a selected base_movable part with a CalcView hook as well. Requires OverrideEyeAngles, OverrideAngles, a valid base_movable OverrideEyePositionPart and user consent"})
+ :GetSetPart("OverrideEyePositionPart")
+ :GetSet("DrawLocalPlayer", true, {description = "Whether the resulting calcview will draw the target player as in third person, otherwise hide the player"})
+
+ :SetPropertyGroup("Targets")
+ :GetSet("AffectPlayerOwner", false)
+ :GetSet("Players", false)
+ :GetSet("PhysicsProps", false)
+ :GetSet("NPC", false)
+
+
+BUILDER:EndStorableVars()
+
+local function set_eyeang(ply, self)
+ if ply ~= pac.LocalPlayer or self:GetPlayerOwner() ~= ply then return end
+ if not convar_lock_aim:GetBool() then
+ self:SetWarning("lock part aiming is disabled on this server!")
+ return
+ end
+ local plyang = ply:EyeAngles()
+ local pos, ang = self:GetDrawPosition()
+
+ if self.Mode == "SetEyeang" then
+ ang.r = 0
+ if not self.AffectPitch then ang.p = plyang.p end
+ if not self.AffectYaw then ang.y = plyang.y end
+ elseif self.Mode == "AimToPos" then
+ ang = (pos - ply:EyePos()):Angle()
+ if not self.AffectPitch then ang.p = plyang.p end
+ if not self.AffectYaw then ang.y = plyang.y end
+ end
+ if self.SmoothAiming then
+ local lerped_ang = LerpAngle(self.SmoothFraction, plyang, ang)
+ lerped_ang.r = 0
+ ply:SetEyeAngles(lerped_ang)
+ else
+ ply:SetEyeAngles(ang)
+ end
+end
+
+function PART:OnThink()
+ if not convar_lock:GetBool() then return end
+ if util.NetworkStringToID("pac_request_position_override_on_entity_grab") == 0 then self:SetError("This part is deactivated on the server") return end
+ pac.Blocked_Combat_Parts = pac.Blocked_Combat_Parts or {}
+ if pac.Blocked_Combat_Parts then
+ if pac.Blocked_Combat_Parts[self.ClassName] then return end
+ end
+
+ if self.forcebreak then
+ if self.next_allowed_grab < CurTime() then --we're able to resume
+ if self.ContinuousSearch then
+ self.forcebreak = false
+ else
+ --wait for the next showing to reset the search because we have self.resetcondition
+ end
+ else
+ return
+ end
+ end
+
+ if self.Mode == "Grab" then
+ if not convar_lock_grab:GetBool() then return end
+ if pac.Blocked_Combat_Parts then
+ if pac.Blocked_Combat_Parts[self.ClassName] then
+ return
+ end
+ end
+ if self.ContinuousSearch and not self.grabbing then
+ self:DecideTarget()
+ end
+ self:CheckEntValidity()
+ if self.valid_ent then
+ local final_ang = Angle(0, 0, 0)
+ if self.OverrideAngles then --if overriding angles
+ if self.is_first_time then
+ self.default_ang = self.target_ent:GetAngles() --record the initial ent angles
+ end
+ if self.OverrideEyeAngles then self.default_ang.y = self:GetWorldAngles().y end --if we want to override players eye angles we will keep recording the yaw
+
+ elseif not self.grabbing then
+ self.default_ang = self.target_ent:GetAngles() --record the initial ent angles anyway
+ end
+
+ local relative_transform_matrix = Matrix()
+ relative_transform_matrix:Identity()
+
+ if self.RelativeGrab then
+ if self.is_first_time then self:CalculateRelativeOffset() end
+ relative_transform_matrix = self.relative_transform_matrix or Matrix():Identity()
+ else
+ relative_transform_matrix = Matrix()
+ relative_transform_matrix:Identity()
+ end
+
+ local offset_matrix = Matrix()
+ offset_matrix:Translate(self:GetWorldPosition())
+ offset_matrix:Rotate(self:GetWorldAngles())
+ offset_matrix:Mul(relative_transform_matrix)
+
+ local relative_offset_pos = offset_matrix:GetTranslation()
+ local relative_offset_ang = offset_matrix:GetAngles()
+
+ local ply_owner = self:GetPlayerOwner()
+
+ if pac.LocalPlayer == ply_owner then
+ if not convar_combat_enforce_netrate:GetBool() then
+ if not pac.CountNetMessage() then self:SetInfo("Went beyond the allowance") return end
+ end
+ net.Start("pac_request_position_override_on_entity_grab")
+ net.WriteBool(self.is_first_time)
+ net.WriteString(self.UniqueID)
+ if self.RelativeGrab then
+ net.WriteVector(relative_offset_pos)
+ net.WriteAngle(relative_offset_ang)
+ else
+ net.WriteVector(self:GetWorldPosition())
+ net.WriteAngle(self:GetWorldAngles())
+ end
+ end
+
+ local try_override_eyeang = false
+ if self.target_ent:IsPlayer() then
+ if self.OverrideEyeAngles then try_override_eyeang = true end
+ end
+ if pac.LocalPlayer == ply_owner then
+ net.WriteBool(self.OverrideAngles)
+ net.WriteBool(try_override_eyeang)
+ net.WriteBool(self.NoCollide)
+ net.WriteEntity(self.target_ent)
+ net.WriteEntity(self:GetRootPart():GetOwner())
+ local can_calcview = false
+ if self.OverrideEyePosition and IsValid(self.OverrideEyePositionPart) then
+ if self.OverrideEyePositionPart.GetWorldAngles then
+ can_calcview = true
+ end
+ end
+ net.WriteBool(can_calcview)
+ --print(IsValid(self.OverrideEyePositionPart), self.OverrideEyeAngles)
+ if can_calcview then
+ net.WriteVector(self.OverrideEyePositionPart:GetWorldPosition())
+ net.WriteAngle(self.OverrideEyePositionPart:GetWorldAngles())
+ else
+ net.WriteVector(self:GetWorldPosition())
+ net.WriteAngle(self:GetWorldAngles())
+ end
+ net.WriteBool(self.DrawLocalPlayer)
+ net.SendToServer()
+ end
+ --print(self:GetRootPart():GetOwner())
+ if self.Players and self.target_ent:IsPlayer() and self.OverrideAngles then
+ local mat = Matrix()
+ mat:Identity()
+
+ if self.OverrideAngles then
+ final_ang = self:GetWorldAngles()
+ end
+ if self.OverrideEyeAngles then
+ final_ang = self:GetWorldAngles()
+ --final_ang = Angle(0,180,0)
+ --print("chose part ang")
+ end
+ if self.OverrideEyePosition and can_calcview then
+ final_ang = self.OverrideEyePositionPart:GetWorldAngles()
+ --print("chose alt part ang")
+ end
+
+ local eyeang = self.target_ent:EyeAngles()
+ --print("eyeang", eyeang)
+ eyeang.p = 0
+ eyeang.y = eyeang.y
+ eyeang.r = 0
+ mat:Rotate(final_ang - eyeang) --this works
+ --mat:Rotate(eyeang)
+ --print("transform ang", final_ang)
+ --print("part's angles", self:GetWorldAngles())
+ --mat:Rotate(self:GetWorldAngles())
+
+ self.target_ent:EnableMatrix("RenderMultiply", mat)
+ end
+
+ self.grabbing = true
+ self.teleported = false
+ end
+ elseif self.Mode == "SetEyeang" then
+ if not self.ContinuousAim then return end
+ set_eyeang(self:GetPlayerOwner(), self)
+ elseif self.Mode == "AimToPos" then
+ if not self.ContinuousAim then return end
+ set_eyeang(self:GetPlayerOwner(), self)
+ end
+ --if self.is_first_time then print("lock " .. self.UniqueID .. "did its first clock") end
+ self.is_first_time = false
+end
+
+do
+ function PART:BreakLock(ent)
+ self.forcebreak = true
+ self.next_allowed_grab = CurTime() + 3
+ if self.target_ent then self.target_ent.IsGrabbedByUID = nil end
+ self.target_ent = nil
+ self.grabbing = false
+ pac.Message(Color(255, 50, 50), "lock break result:")
+ MsgC(Color(0,255,255), "\t", self) MsgC(Color(200, 200, 200), " in your group ") MsgC(Color(0,255,255), self:GetRootPart(),"\n")
+ MsgC(Color(200, 200, 200), "\tIt will now be in the forcebreak state until the next allowed grab, 3 seconds from now\nalso this entity can't be grabbed for 10 seconds.\n")
+ if not self.ContinuousSearch then
+ self.resetcondition = true
+ end
+
+ ent:SetGravity(1)
+
+ ent.pac_recently_broken_free_from_lock = CurTime()
+ ent:DisableMatrix("RenderMultiply")
+ end
+ net.Receive("pac_request_lock_break", function(len)
+ --[[format:
+ net.Start("pac_request_lock_break")
+ net.WriteEntity(ply) --the breaker
+ net.WriteString("") --the uid if applicable
+ net.Send(ent) --that's us! the locker
+ ]]
+ local target_to_release = net.ReadEntity()
+ local uid = net.ReadString()
+ local reason = net.ReadString()
+ pac.Message(Color(255, 255, 255), "------------ CEASE AND DESIST! / BREAK LOCK ------------")
+ MsgC(Color(0,255,255), tostring(target_to_release)) MsgC(Color(255,50,50), " WANTS TO BREAK FREE!!\n")
+ MsgC(Color(255,50,50), "reason:") MsgC(Color(0,255,255), reason .."\n")
+
+ if uid ~= "" then --if a uid is provided
+ MsgC(Color(255, 50, 50), "AND IT KNOWS YOUR UID! " .. uid .. "\n")
+ local part = pac.GetPartFromUniqueID(pac.Hash(pac.LocalPlayer), uid)
+ if part then
+ if part.ClassName == "lock" then
+ part:BreakLock(target_to_release)
+ end
+ end
+ else
+ MsgC(Color(200, 200, 200), "NOW! WE SEARCH YOUR LOCAL PARTS!\n")
+ for i,part in pairs(pac.GetLocalParts()) do
+ if part.ClassName == "lock" then
+ if part.grabbing then
+ if IsValid(part.target_ent) and part.target_ent == target_to_release then
+ part:BreakLock(target_to_release)
+ end
+ end
+ end
+ end
+ end
+
+ end)
+
+ net.Receive("pac_mark_grabbed_ent", function(len)
+
+ local target_to_mark = net.ReadEntity()
+ if not IsValid(target_to_mark) then return end
+ if target_to_mark:EntIndex() == 0 then return end
+ local successful_grab = net.ReadBool()
+ local uid = net.ReadString()
+ local part = pac.GetPartFromUniqueID(pac.Hash(pac.LocalPlayer), uid)
+ --print(target_to_mark,"is grabbed by",uid)
+
+ if not successful_grab then
+ part:BreakLock(target_to_mark) --yes we will employ the aggressive lock break here
+ else
+ target_to_mark.IsGrabbed = successful_grab
+ target_to_mark.IsGrabbedByUID = uid
+ target_to_mark:SetGravity(0)
+ end
+ end)
+end
+
+function PART:SetRadius(val)
+ self.Radius = val
+ local sv_dist = convar_lock_max_grab_radius:GetInt()
+ if self.Radius > sv_dist then
+ self:SetInfo("Your radius is beyond the server's maximum permitted! Server max is " .. sv_dist)
+ else
+ self:SetInfo(nil)
+ end
+end
+
+function PART:OnShow()
+ if util.NetworkStringToID("pac_request_position_override_on_entity_grab") == 0 then self:SetError("This part is deactivated on the server") return end
+ local origin_part
+ self.is_first_time = true
+ if self.resetting_condition or self.forcebreak then
+ if self.next_allowed_grab < CurTime() then
+ self.forcebreak = false
+ self.resetting_condition = false
+ end
+ end
+ local hookID = "pace_draw_lockpart_preview" .. self.UniqueID
+ pac.AddHook("PostDrawOpaqueRenderables", hookID, function()
+ if not IsValid(self) then pac.RemoveHook("PostDrawOpaqueRenderables", hookID) return end
+ if self.TargetPart:IsValid() then
+ origin_part = self.TargetPart
+ else
+ origin_part = self
+ end
+ if origin_part == nil or not self.Preview or pac.LocalPlayer ~= self:GetPlayerOwner() then return end
+ local sv_dist = convar_lock_max_grab_radius:GetInt()
+
+ render.DrawLine(origin_part:GetWorldPosition(),origin_part:GetWorldPosition() + Vector(0,0,-self.OffsetDownAmount),Color(255,255,255))
+
+ if self.Radius < sv_dist then
+ self:SetInfo(nil)
+ render.DrawWireframeSphere(origin_part:GetWorldPosition() + Vector(0,0,-self.OffsetDownAmount), sv_dist, 30, 30, Color(50,50,150),true)
+ render.DrawWireframeSphere(origin_part:GetWorldPosition() + Vector(0,0,-self.OffsetDownAmount), self.Radius, 30, 30, Color(255,255,255),true)
+ else
+ self:SetInfo("Your radius is beyond the server max! Active max is " .. sv_dist)
+ render.DrawWireframeSphere(origin_part:GetWorldPosition() + Vector(0,0,-self.OffsetDownAmount), sv_dist, 30, 30, Color(0,255,255),true)
+ render.DrawWireframeSphere(origin_part:GetWorldPosition() + Vector(0,0,-self.OffsetDownAmount), self.Radius, 30, 30, Color(100,100,100),true)
+ end
+
+ end)
+ if self.Mode == "Teleport" then
+ if not convar_lock_teleport:GetBool() or pac.Blocked_Combat_Parts[self.ClassName] then return end
+ if pace.still_loading_wearing then return end
+ self.target_ent = nil
+
+ local ang_yaw_only = self:GetWorldAngles()
+ ang_yaw_only.p = 0
+ ang_yaw_only.r = 0
+ if pac.LocalPlayer == self:GetPlayerOwner() then
+
+ local teleport_pos_final = self:GetWorldPosition()
+
+ if self.ClampDistance then
+ local ply_pos = self:GetPlayerOwner():GetPos()
+ local pos = self:GetWorldPosition()
+
+ if pos:Distance(ply_pos) > self.Radius then
+ local clamped_pos = ply_pos + (pos - ply_pos):GetNormalized()*self.Radius
+ teleport_pos_final = clamped_pos
+ end
+ end
+ if self.SlopeSafety then teleport_pos_final = teleport_pos_final + Vector(0,0,30) end
+ if not convar_combat_enforce_netrate:GetBool() then
+ if not pac.CountNetMessage() then self:SetInfo("Went beyond the allowance") return end
+ end
+ timer.Simple(0, function()
+ if self:IsHidden() or self:IsDrawHidden() then return end
+ net.Start("pac_request_position_override_on_entity_teleport")
+ net.WriteString(self.UniqueID)
+ net.WriteVector(teleport_pos_final)
+ net.WriteAngle(ang_yaw_only)
+ net.WriteBool(self.OverrideAngles)
+ net.SendToServer()
+ end)
+
+ end
+ self.grabbing = false
+ elseif self.Mode == "Grab" then
+ self:DecideTarget()
+ self:CheckEntValidity()
+ elseif self.Mode == "SetEyeang" then
+ set_eyeang(self:GetPlayerOwner(), self)
+ elseif self.Mode == "AimToPos" then
+ set_eyeang(self:GetPlayerOwner(), self)
+ end
+end
+
+function PART:OnDoubleClickSpecified()
+ if self.Mode ~= "Grab" then
+ self:OnShow()
+ end
+end
+
+function PART:OnHide()
+ pac.RemoveHook("PostDrawOpaqueRenderables", "pace_draw_lockpart_preview"..self.UniqueID)
+ self.teleported = false
+ self.grabbing = false
+ if self.target_ent == nil then return
+ else self.target_ent.IsGrabbed = false self.target_ent.IsGrabbedByUID = nil end
+ if util.NetworkStringToID( "pac_request_position_override_on_entity_grab" ) == 0 then self:SetError("This part is deactivated on the server") return end
+ self:reset_ent_ang()
+end
+
+function PART:reset_ent_ang()
+ if self.target_ent == nil then return end
+ local reset_ent = self.target_ent
+
+ if reset_ent:IsValid() then
+ timer.Simple(math.min(self.RestoreDelay,5), function()
+ if pac.LocalPlayer == self:GetPlayerOwner() then
+ if not convar_combat_enforce_netrate:GetBool() then
+ if not pac.CountNetMessage() then self:SetInfo("Went beyond the allowance") return end
+ end
+ net.Start("pac_request_angle_reset_on_entity")
+ net.WriteAngle(Angle(0,0,0))
+ net.WriteFloat(self.RestoreDelay)
+ net.WriteEntity(reset_ent)
+ net.WriteEntity(self:GetPlayerOwner())
+ net.SendToServer()
+ end
+ if self.Players and reset_ent:IsPlayer() then
+ reset_ent:DisableMatrix("RenderMultiply")
+ end
+ end)
+ end
+end
+
+function PART:OnRemove()
+end
+
+function PART:DecideTarget()
+
+ local RADIUS = math.Clamp(self.Radius,0,GetConVar("pac_sv_lock_max_grab_radius"):GetInt())
+ local ents_candidates = {}
+ local chosen_ent = nil
+ local target_part = self.TargetPart
+ local origin
+
+ if self.TargetPart and (self.TargetPart):IsValid() then
+ origin = (self.TargetPart):GetWorldPosition()
+ else
+ origin = self:GetWorldPosition()
+ end
+ origin:Add(Vector(0,0,-self.OffsetDownAmount))
+
+ for i, ent_candidate in ipairs(ents.GetAll()) do
+
+ if IsValid(ent_candidate) then
+ local check_further = true
+ if ent_candidate.pac_recently_broken_free_from_lock then
+ if ent_candidate.pac_recently_broken_free_from_lock + 10 > CurTime() then
+ check_further = false
+ end
+ else check_further = true end
+
+ if check_further then
+ if ent_candidate:GetPos():Distance( origin ) < RADIUS then
+ if self.Players and ent_candidate:IsPlayer() then
+ --we don't want to grab ourselves
+ if (ent_candidate ~= self:GetRootPart():GetOwner()) or (self.AffectPlayerOwner and ent_candidate == self:GetPlayerOwner()) then
+ if not (not self.AffectPlayerOwner and ent_candidate == self:GetPlayerOwner()) then
+ chosen_ent = ent_candidate
+ table.insert(ents_candidates, ent_candidate)
+ end
+ elseif (self:GetPlayerOwner() ~= ent_candidate) then --if it's another player, good
+ chosen_ent = ent_candidate
+ table.insert(ents_candidates, ent_candidate)
+ end
+ elseif self.PhysicsProps and (physics_point_ent_classes[ent_candidate:GetClass()] or string.find(ent_candidate:GetClass(),"item_") or string.find(ent_candidate:GetClass(),"ammo_")) then
+ chosen_ent = ent_candidate
+ table.insert(ents_candidates, ent_candidate)
+ elseif self.NPC and (ent_candidate:IsNPC() or ent_candidate:IsNextBot() or ent_candidate.IsDrGEntity or ent_candidate.IsVJBaseSNPC) then
+ chosen_ent = ent_candidate
+ table.insert(ents_candidates, ent_candidate)
+ end
+ end
+ end
+ end
+ end
+ local closest_distance = math.huge
+
+ --sort for the closest
+ for i,ent_candidate in ipairs(ents_candidates) do
+ local test_distance = (ent_candidate:GetPos()):Distance( self:GetWorldPosition())
+ if (test_distance < closest_distance) then
+ closest_distance = test_distance
+ chosen_ent = ent_candidate
+ end
+ end
+
+ if chosen_ent ~= nil then
+ self.target_ent = chosen_ent
+ if pac.LocalPlayer == self:GetPlayerOwner() then
+ print("selected ", chosen_ent, "dist ", (chosen_ent:GetPos()):Distance( self:GetWorldPosition() ))
+ end
+ self.valid_ent = true
+ else
+ self.target_ent = nil
+ self.valid_ent = false
+ end
+end
+
+
+
+function PART:CheckEntValidity()
+
+ if self.target_ent == nil then
+ self.valid_ent = false
+ elseif self.target_ent:EntIndex() == 0 then
+ self.valid_ent = false
+ elseif IsValid(self.target_ent) then
+ self.valid_ent = true
+ end
+ if self.target_ent ~= nil then
+ if self.target_ent.IsGrabbedByUID and self.target_ent.IsGrabbedByUID ~= self.UniqueID then self.valid_ent = false end
+ end
+ if not self.valid_ent then self.target_ent = nil end
+ --print("ent check:",self.valid_ent)
+end
+
+function PART:CalculateRelativeOffset()
+ if self.target_ent == nil or not IsValid(self.target_ent) then self.relative_transform_matrix = Matrix() return end
+ self.relative_transform_matrix = Matrix()
+ self.relative_transform_matrix:Rotate(self.target_ent:GetAngles() - self:GetWorldAngles())
+ self.relative_transform_matrix:Translate(self.target_ent:GetPos() - self:GetWorldPosition())
+ --print("ang delta!", self.target_ent:GetAngles() - self:GetWorldAngles())
+end
+
+function PART:Initialize()
+ self.default_ang = Angle(0,0,0)
+ if not convar_lock_grab:GetBool() then
+ if not convar_lock_teleport:GetBool() then
+ self:SetWarning("lock part grabs and teleports are disabled on this server!")
+ else
+ self:SetWarning("lock part grabs are disabled on this server!")
+ end
+ end
+ if not convar_lock_teleport:GetBool() then
+ if not convar_lock_grab:GetBool() then
+ self:SetWarning("lock part grabs and teleports are disabled on this server!")
+ else
+ self:SetWarning("lock part teleports are disabled on this server!")
+ end
+ end
+ if not convar_lock:GetBool() then self:SetError("lock parts are disabled on this server!") end
+end
+
+
+BUILDER:Register()
diff --git a/lua/pac3/core/client/parts/material.lua b/lua/pac3/core/client/parts/material.lua
index 0d49d2a02..8543f7761 100644
--- a/lua/pac3/core/client/parts/material.lua
+++ b/lua/pac3/core/client/parts/material.lua
@@ -1,6 +1,7 @@
local shader_params = include("pac3/libraries/shader_params.lua")
local mat_hdr_level = GetConVar("mat_hdr_level")
+local dump_vmt_when_load_vmt = CreateConVar("pac_material_dump_vmt", "2", FCVAR_ARCHIVE, "whether to print the VMT information when using the Load VMT field in materials\n0= don't\n1 = print only the path as a single line\n2 = full prints for the raw VMT and extracted table")
local material_flags = {
debug = bit.lshift(1, 0),
@@ -120,6 +121,9 @@ for shader_name, groups in pairs(shader_params.shaders) do
BUILDER:GetSet("LoadVmt", "", {editor_panel = "material"})
function PART:SetLoadVmt(path)
if not path or path == "" then return end
+ if (self.Notes == "") or (string.sub(self.Notes, 1, 15) == "last loaded VMT") then
+ self:SetNotes("last loaded VMT: " .. path)
+ end
local str = file.Read("materials/" .. path .. ".vmt", "GAME")
@@ -139,11 +143,16 @@ for shader_name, groups in pairs(shader_params.shaders) do
end
end
- print(str)
- print("======")
- PrintTable(vmt)
- print("======")
-
+ if dump_vmt_when_load_vmt:GetInt() == 1 then
+ print("====== VMT loaded: " .. path)
+ elseif dump_vmt_when_load_vmt:GetInt() == 2 then
+ print("\n====== " .. path .. " raw VMT text:\n")
+ print(str)
+ print("====== extracted table:\n")
+ PrintTable(vmt)
+ print("\n======")
+ end
+ local errors = {"cannot convert material parameter:"}
for k,v in pairs(vmt) do
if k:StartWith("$") then k = k:sub(2) end
@@ -175,9 +184,13 @@ for shader_name, groups in pairs(shader_params.shaders) do
func(self, v)
else
- pac.Message("cannot convert material parameter " .. k)
+ table.insert(errors,k)
+ if dump_vmt_when_load_vmt:GetInt() == 2 then
+ pac.Message("cannot convert material parameter " .. k)
+ end
end
end
+ if #errors > 1 then self:SetWarning(table.concat(errors, "\n")) else self:SetWarning() end
end
BUILDER:GetSet("MaterialOverride", "all", {enums = function(self, str)
@@ -256,6 +269,10 @@ for shader_name, groups in pairs(shader_params.shaders) do
function PART:Initialize()
self.translation_vector = Vector()
self.rotation_angle = Angle(0, 0, 0)
+ timer.Simple(0, function()
+ self:SetbasetexturetransformAngle(self:GetbasetexturetransformAngle())
+ self:SetHide(self:GetHide())
+ end)
end
diff --git a/lua/pac3/core/client/parts/model.lua b/lua/pac3/core/client/parts/model.lua
index d8fe5de4a..8619a82ab 100644
--- a/lua/pac3/core/client/parts/model.lua
+++ b/lua/pac3/core/client/parts/model.lua
@@ -5,25 +5,26 @@ CreateConVar( "pac_model_max_scales", "10000", FCVAR_ARCHIVE, "Maximum scales mo
local pac = pac
-local render_SetColorModulation = render.SetColorModulation
-local render_SetBlend = render.SetBlend
-local render_CullMode = render.CullMode
-local MATERIAL_CULLMODE_CW = MATERIAL_CULLMODE_CW
-local MATERIAL_CULLMODE_CCW = MATERIAL_CULLMODE_CCW
-local render_MaterialOverride = render.ModelMaterialOverride
-local cam_PushModelMatrix = cam.PushModelMatrix
+local cam = cam
local cam_PopModelMatrix = cam.PopModelMatrix
-local Vector = Vector
-local EF_BONEMERGE = EF_BONEMERGE
-local NULL = NULL
+local cam_PushModelMatrix = cam.PushModelMatrix
local Color = Color
+local EF_BONEMERGE = EF_BONEMERGE
+local MATERIAL_CULLMODE_CCW = MATERIAL_CULLMODE_CCW
+local MATERIAL_CULLMODE_CW = MATERIAL_CULLMODE_CW
local Matrix = Matrix
-local vector_origin = vector_origin
+local NULL = NULL
local render = render
-local cam = cam
-local surface = surface
+local render_CullMode = render.CullMode
+local render_MaterialOverride = render.ModelMaterialOverride
local render_MaterialOverrideByIndex = render.MaterialOverrideByIndex
+local render_RenderFlashlights = render.RenderFlashlights
+local render_SetBlend = render.SetBlend
+local render_SetColorModulation = render.SetColorModulation
local render_SuppressEngineLighting = render.SuppressEngineLighting
+local surface = surface
+local Vector = Vector
+local vector_origin = vector_origin
local BUILDER, PART = pac.PartTemplate("base_drawable")
@@ -32,10 +33,10 @@ PART.ClassName = "model2"
PART.Category = "model"
PART.ManualDraw = true
PART.HandleModifiersManually = true
-PART.Icon = 'icon16/shape_square.png'
+PART.Icon = "icon16/shape_square.png"
PART.is_model_part = true
PART.ProperColorRange = true
-PART.Group = 'model'
+PART.Group = "model"
BUILDER:StartStorableVars()
:SetPropertyGroup("generic")
@@ -96,23 +97,6 @@ function PART:GetDynamicProperties()
}
end
- for _, info in ipairs(ent:GetBodyGroups()) do
- if info.num > 1 then
- tbl[info.name] = {
- key = info.name,
- set = function(val)
- local tbl = self:ModelModifiersToTable(self:GetModelModifiers())
- tbl[info.name] = val
- self:SetModelModifiers(self:ModelModifiersToString(tbl))
- end,
- get = function()
- return self:ModelModifiersToTable(self:GetModelModifiers())[info.name] or 0
- end,
- udata = {editor_onchange = function(self, num) return math.Clamp(math.Round(num), 0, info.num - 1) end, group = "bodygroups"},
- }
- end
- end
-
if ent:GetMaterials() and #ent:GetMaterials() > 1 then
for i, name in ipairs(ent:GetMaterials()) do
name = name:match(".+/(.+)") or name
@@ -137,6 +121,30 @@ function PART:GetDynamicProperties()
end
end
+ for _, info in ipairs(ent:GetBodyGroups()) do
+ if info.num > 1 then
+ local bodygroup_name = info.name
+ local exception = tbl[info.name] ~= nil --trouble! an existing material competes with the bodygroup, we should try renaming it?
+ if exception then
+ bodygroup_name = "_" .. info.name
+ self.bodygroup_exceptions[info.name] = true
+ end
+
+ tbl[bodygroup_name] = {
+ key = bodygroup_name,
+ set = function(val)
+ local tbl = self:ModelModifiersToTable(self:GetModelModifiers())
+ tbl[bodygroup_name] = val
+ self:SetModelModifiers(self:ModelModifiersToString(tbl))
+ end,
+ get = function()
+ return self:ModelModifiersToTable(self:GetModelModifiers())[bodygroup_name] or 0
+ end,
+ udata = {editor_onchange = function(self, num) return math.Clamp(math.Round(num), 0, info.num - 1) end, group = "bodygroups"},
+ }
+ end
+ end
+
return tbl
end
@@ -176,7 +184,7 @@ end
function PART:ModelModifiersToString(tbl)
local str = ""
- for k,v in pairs(tbl) do
+ for k, v in pairs(tbl) do
str = str .. k .. "=" .. v .. ";"
end
return str
@@ -198,11 +206,22 @@ function PART:SetModelModifiers(str)
if not owner:GetBodyGroups() then return end
self.draw_bodygroups = {}
+ self.bodygroup_exceptions = self.bodygroup_exceptions or {}
+ local dyn_props = self:GetDynamicProperties()
for i, info in ipairs(owner:GetBodyGroups()) do
local val = tbl[info.name]
- if val then
- table.insert(self.draw_bodygroups, {info.id, val})
+ if self.bodygroup_exceptions[info.name] then
+ if dyn_props["_"..info.name] then
+ val = dyn_props["_"..info.name].get()
+ end
+ if val then
+ table.insert(self.draw_bodygroups, {info.id, val})
+ end
+ else
+ if val then
+ table.insert(self.draw_bodygroups, {info.id, val})
+ end
end
end
end
@@ -289,6 +308,7 @@ function PART:Initialize()
self.Owner:SetNoDraw(true)
self.Owner.PACPart = self
self.material_count = 0
+ self.bodygroup_exceptions = {}
end
function PART:OnShow()
@@ -304,7 +324,7 @@ end
function PART:OnRemove()
if not self.loading then
- SafeRemoveEntityDelayed(self.Owner,0.1)
+ SafeRemoveEntityDelayed(self.Owner, 0.1)
end
end
@@ -433,12 +453,11 @@ end
local matrix = Matrix()
-local IDENT_SCALE = Vector(1,1,1)
local _self, _ent, _pos, _ang
local function ent_draw_model(self, ent, pos, ang)
if self.obj_mesh then
- ent:SetModelScale(0,0)
+ ent:SetModelScale(0.001, 0)
ent:DrawModel()
matrix:Identity()
@@ -485,14 +504,12 @@ function PART:DrawModel(ent, pos, ang)
_self, _ent, _pos, _ang = self, ent, pos, ang
if self.ClassName ~= "entity2" then
- render.PushFlashlightMode(true)
-
- material_bound = self:BindMaterials(ent) or material_bound
- ent.pac_drawing_model = true
- ProtectedCall(protected_ent_draw_model)
- ent.pac_drawing_model = false
-
- render.PopFlashlightMode()
+ render_RenderFlashlights(function()
+ material_bound = self:BindMaterials(ent) or material_bound
+ ent.pac_drawing_model = true
+ ProtectedCall(protected_ent_draw_model)
+ ent.pac_drawing_model = false
+ end)
end
if self.NoCulling then
@@ -536,7 +553,7 @@ function PART:DrawLoadingText(ent, pos)
cam.End2D()
end
-local ALLOW_TO_MDL = CreateConVar('pac_allow_mdl', '1', CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Allow to use custom MDLs')
+local ALLOW_TO_MDL = CreateConVar("pac_allow_mdl", "1", CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow to use custom MDLs")
function PART:RefreshModel()
if self.refreshing_model then return end
@@ -578,7 +595,7 @@ end
local function RealDrawModel(self, ent, pos, ang)
if self.Mesh then
- ent:SetModelScale(0,0)
+ ent:SetModelScale(0.001, 0)
ent:DrawModel()
local matrix = Matrix()
@@ -607,7 +624,7 @@ function PART:ProcessModelChange()
if path:find("://", nil, true) then
if path:StartWith("objhttp") or path:StartWith("obj:http") or path:match("%.obj%p?") or self.ForceObjUrl then
- path = path:gsub("^objhttp","http"):gsub("^obj:http","http")
+ path = path:gsub("^objhttp", "http"):gsub("^obj:http", "http")
self.loading = "downloading obj"
pac.urlobj.GetObjFromURL(path, false, false,
@@ -669,7 +686,7 @@ function PART:ProcessModelChange()
end
)
else
- local status, reason = hook.Run('PAC3AllowMDLDownload', self:GetPlayerOwner(), self, path)
+ local status, reason = hook.Run("PAC3AllowMDLDownload", self:GetPlayerOwner(), self, path)
if ALLOW_TO_MDL:GetBool() and status ~= false then
self.loading = "downloading mdl zip"
@@ -722,7 +739,7 @@ function PART:SetModel(path)
self:ProcessModelChange()
end
-local NORMAL = Vector(1,1,1)
+local NORMAL = Vector(1, 1, 1)
function PART:CheckScale()
local owner = self:GetOwner()
@@ -756,7 +773,7 @@ function PART:SetScale(vec)
if largest_scale > 10000 then --warn about the default max scale
self:SetError("Scale is being limited due to having an excessive component. Default maximum values are 10000")
else self:SetError() end --if ok, clear the warning
- vec = vec or Vector(1,1,1)
+ vec = vec or Vector(1, 1, 1)
self.Scale = vec
@@ -765,7 +782,7 @@ function PART:SetScale(vec)
end
end
-local vec_one = Vector(1,1,1)
+local vec_one = Vector(1, 1, 1)
function PART:ApplyMatrix()
local ent = self:GetOwner()
diff --git a/lua/pac3/core/client/parts/model/entity.lua b/lua/pac3/core/client/parts/model/entity.lua
index 793838448..ef9294719 100644
--- a/lua/pac3/core/client/parts/model/entity.lua
+++ b/lua/pac3/core/client/parts/model/entity.lua
@@ -41,6 +41,7 @@ function PART:SetDrawShadow(b)
local ent = self:GetOwner()
if not ent:IsValid() then return end
+ pac.emut.MutateEntity(self:GetPlayerOwner(), "draw_shadow", ent, b)
ent:DrawShadow(b)
ent:MarkShadowAsDirty()
end
@@ -211,6 +212,7 @@ function PART:OnRemove()
local player_owner = self:GetPlayerOwner()
pac.emut.RestoreMutations(player_owner, "model", ent)
+ pac.emut.RestoreMutations(player_owner, "draw_shadow", ent)
if ent:IsPlayer() or ent:IsNPC() then
pac.emut.RestoreMutations(player_owner, "size", ent)
diff --git a/lua/pac3/core/client/parts/model/weapon.lua b/lua/pac3/core/client/parts/model/weapon.lua
index ba49afdc5..2a827cb18 100644
--- a/lua/pac3/core/client/parts/model/weapon.lua
+++ b/lua/pac3/core/client/parts/model/weapon.lua
@@ -158,4 +158,8 @@ function PART:OnHide()
end
end
+function PART:OnRemove()
+ --overridden to prevent calling SafeRemoveEntityDelayed on a weapon entity (not a pac CS Ent)
+end
+
BUILDER:Register()
\ No newline at end of file
diff --git a/lua/pac3/core/client/parts/movement.lua b/lua/pac3/core/client/parts/movement.lua
index e5fb4de5e..bc5fd4b8b 100644
--- a/lua/pac3/core/client/parts/movement.lua
+++ b/lua/pac3/core/client/parts/movement.lua
@@ -16,7 +16,8 @@ local function ADD(PART, name, default, ...)
PART["Set" .. name] = function(self, val)
self[name] = val
- local ply = self:GetRootPart():GetOwner()
+ local ply = self:GetPlayerOwner()
+ --if ply:GetClass() == "viewmodel" then ply = self:GetRootPart():GetOwner() end
if ply == pac.LocalPlayer then
@@ -44,6 +45,8 @@ BUILDER:StartStorableVars()
BUILDER:SetPropertyGroup("generic")
ADD(PART, "Noclip", false)
ADD(PART, "Gravity", Vector(0, 0, -600))
+ ADD(PART, "Mass", 85)
+ BUILDER:GetSet("PreserveInFirstPerson", false, {description = "keeps the movement modification active in first person"})
BUILDER:SetPropertyGroup("movement")
ADD(PART, "SprintSpeed", 400)
@@ -60,7 +63,10 @@ BUILDER:StartStorableVars()
BUILDER:SetPropertyGroup("air")
ADD(PART, "AllowZVelocity", false)
ADD(PART, "AirFriction", 0.01, {editor_clamp = {0, 1}, editor_sensitivity = 0.1})
- ADD(PART, "MaxAirSpeed", 1)
+ ADD(PART, "HorizontalAirFrictionMultiplier", 1, {editor_clamp = {0, 1}, editor_sensitivity = 0.1})
+ ADD(PART, "MaxAirSpeed", 750)
+ ADD(PART, "StrafingStrengthMultiplier", 1)
+
BUILDER:SetPropertyGroup("view angles")
ADD(PART, "ReversePitch", false)
@@ -94,9 +100,10 @@ function PART:GetNiceName()
end
function PART:OnShow()
- local ent = self:GetRootPart():GetOwner()
+ local ent = self:GetPlayerOwner()
if ent:IsValid() then
+ if ent:GetClass() == "viewmodel" then ent = self:GetPlayerOwner() end
ent.last_movement_part = self:GetUniqueID()
for i,v in ipairs(update_these) do
v(self)
@@ -105,12 +112,24 @@ function PART:OnShow()
end
function PART:OnHide()
- local ent = self:GetRootPart():GetOwner()
-
+ local ent = self:GetRootPart():GetOwner() or self:GetOwner() or self:GetPlayerOwner()
+ if not IsValid(ent) then return end
+ if ent:GetClass() == "viewmodel" then ent = self:GetPlayerOwner() end
+ if not self:IsHidden() and self.PreserveInFirstPerson then return end
if ent == pac.LocalPlayer and ent.last_movement_part == self:GetUniqueID() then
net.Start("pac_modify_movement", true)
net.WriteString("disable")
net.SendToServer()
+ ent.pac_movement = nil
+ end
+end
+
+function PART:OnRemove()
+ local ent = self:GetRootPart():GetOwner()
+ if ent == pac.LocalPlayer then
+ net.Start("pac_modify_movement", true)
+ net.WriteString("disable")
+ net.SendToServer()
ent.pac_movement = nil
end
diff --git a/lua/pac3/core/client/parts/particles.lua b/lua/pac3/core/client/parts/particles.lua
index 0ef3a2ce2..002a663af 100644
--- a/lua/pac3/core/client/parts/particles.lua
+++ b/lua/pac3/core/client/parts/particles.lua
@@ -17,9 +17,6 @@ BUILDER:StartStorableVars()
BUILDER:PropertyOrder("ParentName")
BUILDER:GetSet("Follow", false)
BUILDER:GetSet("Additive", false)
- BUILDER:GetSet("FireOnce", false)
- BUILDER:GetSet("FireDelay", 0.2)
- BUILDER:GetSet("NumberParticles", 1)
BUILDER:GetSet("PositionSpread", 0)
BUILDER:GetSet("PositionSpread2", Vector(0,0,0))
BUILDER:GetSet("DieTime", 3)
@@ -29,9 +26,17 @@ BUILDER:StartStorableVars()
BUILDER:GetSet("EndLength", 0)
BUILDER:GetSet("ParticleAngle", Angle(0,0,0))
BUILDER:GetSet("AddFrametimeLife", false)
+
+ BUILDER:SetPropertyGroup("particle emissions")
+ BUILDER:GetSet("FireDelay", 0.2)
+ BUILDER:GetSet("FireOnce", false)
+ BUILDER:GetSet("NumberParticles", 1, {editor_onchange = function(self,num) return math.Clamp(num,0,2000) end})
+ BUILDER:GetSet("FireDuration", 0, {description = "how long to fire particles\n0 = infinite"})
+ BUILDER:GetSet("Decay", 0, {description = "rate of decay for particle count, in particles per second\n0 = no decay\na positive number means simple decay starting at showtime\na negative number means delayed decay so that it reaches 0 at the time of 'fire duration'"})
+ BUILDER:GetSet("FractionalChance", false, {description = "If 'number particles' has decimals, there is a chance to emit another particle\ne.g. 0.5 is 50% chance to emit a particle\ne.g. 1.25 is 25% chance to fire two / 75% to fire one particle)"})
BUILDER:SetPropertyGroup("stick")
- BUILDER:GetSet("AlignToSurface", true)
- BUILDER:GetSet("StickToSurface", true)
+ BUILDER:GetSet("AlignToSurface", true, {description = "requires 3D set to true"})
+ BUILDER:GetSet("StickToSurface", true, {description = "requires 3D set to true, and sliding set to false"})
BUILDER:GetSet("StickLifetime", 2)
BUILDER:GetSet("StickStartSize", 20)
BUILDER:GetSet("StickEndSize", 0)
@@ -46,11 +51,11 @@ BUILDER:StartStorableVars()
BUILDER:GetSet("Color1", Vector(255, 255, 255), {editor_panel = "color"})
BUILDER:GetSet("RandomColor", false)
BUILDER:GetSet("Lighting", true)
- BUILDER:GetSet("3D", false)
+ BUILDER:GetSet("3D", false, {description = "The particles are oriented relative to the part instead of the viewer.\nYou might want to set zero angle to false if you use this."})
BUILDER:GetSet("DoubleSided", true)
BUILDER:GetSet("DrawManual", false)
BUILDER:SetPropertyGroup("rotation")
- BUILDER:GetSet("ZeroAngle",true)
+ BUILDER:GetSet("ZeroAngle",true, {description = "A workaround for non-3D particles' roll with certain oriented textures. Forces 0,0,0 angles when the particle is emitted\nWith round textures you don't notice, but the same cannot be said of textures which need to be upright rather than having strangely tilted copies."})
BUILDER:GetSet("RandomRollSpeed", 0)
BUILDER:GetSet("RollDelta", 0)
BUILDER:GetSet("ParticleAngleVelocity", Vector(50, 50, 50))
@@ -71,8 +76,14 @@ BUILDER:StartStorableVars()
BUILDER:EndStorableVars()
+function PART:Initialize()
+ self.number_particles = 0
+end
+
function PART:GetNiceName()
- return pac.PrettifyName(("/".. self:GetMaterial()):match(".+/(.+)")) or "error"
+ local str = (self:GetMaterial()):match(".+/(.+)") or ""
+ --return pac.PrettifyName("/".. str) or "error"
+ return "[".. math.Round(self.number_particles or 0,2) .. "] " .. str
end
local function RemoveCallback(particle)
@@ -100,7 +111,7 @@ local function StickCallback(particle, hitpos, normal)
if particle.Align then
local ang = normal:Angle()
ang:RotateAroundAxis(normal, particle:GetAngles().y)
- particle:SetAngles(ang)
+ particle:SetAngles(ang + particle.ParticleAngle + (particle.is_doubleside == true and Angle(180,0,0) or Angle(0,0,0)))
end
if particle.Stick then
@@ -133,8 +144,12 @@ function PART:SetDrawManual(b)
self:GetEmitter():SetNoDraw(b)
end
+local max_active_particles = CreateClientConVar("pac_limit_particles_per_emitter", "8000")
+local max_emit_particles = CreateClientConVar("pac_limit_particles_per_emission", "100")
function PART:SetNumberParticles(num)
- self.NumberParticles = math.Clamp(num, 0, 100)
+ local max = max_emit_particles:GetInt()
+ if num > max or num > 100 then self:SetWarning("You're trying to set the number of particles beyond the pac_limit_particles_per_emission limit, the default limit is 100.\nFor reference, the default max active particles for the emitter is around 8000 but can be further limited with pac_limit_particles_per_emitter") else self:SetWarning() end
+ self.NumberParticles = math.Clamp(num, 0, max)
end
function PART:Set3D(b)
@@ -143,8 +158,10 @@ function PART:Set3D(b)
end
function PART:OnShow(from_rendering)
+ self.number_particles = self.NumberParticles
self.CanKeepFiring = true
self.FirstShot = true
+ self.FirstShotTime = RealTime()
if not from_rendering then
self.NextShot = 0
local pos, ang = self:GetDrawPosition()
@@ -153,7 +170,28 @@ function PART:OnShow(from_rendering)
end
function PART:OnDraw()
- if not self.FireOnce then self.CanKeepFiring = true end
+ self.number_particles = self.NumberParticles or 0
+ if not self.FireOnce then
+ if self.Decay == 0 then
+ self.number_particles = self.NumberParticles or 0
+ elseif self.Decay > 0 then
+ self.number_particles = math.Clamp(self.NumberParticles - (RealTime() - self.FirstShotTime) * self.Decay,0,self.NumberParticles)
+ else
+ self.number_particles = math.Clamp(-self.FireDuration * self.Decay + self.NumberParticles - (RealTime() - self.FirstShotTime) * self.Decay,0,self.NumberParticles)
+ end
+ if self.FireDuration <= 0 then
+ self.CanKeepFiring = true
+ else
+ if RealTime() > self.FirstShotTime + self.FireDuration then self.number_particles = 0 end
+ end
+ if self.Decay ~= 0 then
+ if pace and pace.IsActive() and self.Name == "" then
+ if IsValid(self.pace_tree_node) then
+ self.pace_tree_node:SetText(self:GetNiceName())
+ end
+ end
+ end
+ end
local pos, ang = self:GetDrawPosition()
local emitter = self:GetEmitter()
@@ -210,6 +248,7 @@ function PART:SetMaterial(var)
end
function PART:EmitParticles(pos, ang, real_ang)
+ self.number_particles = self.number_particles or 0
if self.FireOnce and not self.FirstShot then self.CanKeepFiring = false end
local emt = self:GetEmitter()
if not emt then return end
@@ -226,7 +265,19 @@ function PART:EmitParticles(pos, ang, real_ang)
double = 2
end
- for _ = 1, self.NumberParticles do
+ local free_particles = math.max(max_active_particles:GetInt() - emt:GetNumActiveParticles(),0)
+ local max = math.min(free_particles, max_emit_particles:GetInt())
+ --self.number_particles is self.NumberParticles with optional decay applied
+ local fractional_chance = 0
+ if self.FractionalChance then
+ --e.g. treat 0.5 as 50% chance to emit or not
+ local delta = self.number_particles - math.floor(self.number_particles)
+ if math.random() < delta then
+ self.number_particles = self.number_particles + 1
+ end
+ end
+
+ for _ = 1, math.min(self.number_particles,max) do
local mats = self.Material:Split(";")
if #mats > 1 then
self.Materialm = pac.Material(table.Random(mats), self)
@@ -336,18 +387,24 @@ function PART:EmitParticles(pos, ang, real_ang)
if self["3D"] then
if not self.Sliding then
- if i == 1 then
+ if i == 1 and not self.StickToSurface then
particle:SetCollideCallback(RemoveCallback)
else
- particle:SetCollideCallback(StickCallback)
+ if i == 1 then
+ particle:SetCollideCallback(StickCallback)
+ else
+ particle.is_doubleside = true
+ particle:SetCollideCallback(StickCallback)
+ end
end
end
particle:SetAngleVelocity(Angle(self.ParticleAngleVelocity.x, self.ParticleAngleVelocity.y, self.ParticleAngleVelocity.z))
- particle.Align = self.Align
- particle.Stick = self.Stick
- particle.StickLifeTime = self.StickLifeTime
+ particle.ParticleAngle = self.ParticleAngle
+ particle.Align = self.AlignToSurface
+ particle.Stick = self.StickToSurface
+ particle.StickLifeTime = self.StickLifetime
particle.StickStartSize = self.StickStartSize
particle.StickEndSize = self.StickEndSize
particle.StickStartAlpha = self.StickStartAlpha
diff --git a/lua/pac3/core/client/parts/physics.lua b/lua/pac3/core/client/parts/physics.lua
index cc616301e..d6562de06 100644
--- a/lua/pac3/core/client/parts/physics.lua
+++ b/lua/pac3/core/client/parts/physics.lua
@@ -1,3 +1,15 @@
+local physprop_enums = {}
+local physprop_indices = {}
+for i=0,500,1 do
+ local name = util.GetSurfacePropName(i)
+ if name ~= "" then
+ physprop_enums[name] = name
+ physprop_indices[name] = i
+ end
+end
+
+
+
local BUILDER, PART = pac.PartTemplate("base")
PART.ThinkTime = 0
@@ -7,25 +19,44 @@ PART.Group = 'model'
PART.Icon = 'icon16/shape_handles.png'
BUILDER:StartStorableVars()
- BUILDER:GetSet("Box", true)
- BUILDER:GetSet("Radius", 1)
- BUILDER:GetSet("SelfCollision", false)
- BUILDER:GetSet("Gravity", true)
- BUILDER:GetSet("Collisions", true)
- BUILDER:GetSet("Mass", 100)
-
- BUILDER:GetSet("Follow", false)
- BUILDER:GetSet("SecondsToArrive", 0.1)
+ BUILDER:SetPropertyGroup("Behavior")
+ :GetSet("SelfCollision", false)
+ :GetSet("Gravity", true)
+ :GetSet("Collisions", true)
+ :GetSet("ConstrainSphere", 0)
+ :GetSet("Pushable", false, {description = "Whether the physics object should be pushed back by nearby players and props within its radius."})
+ :GetSet("ThinkDelay", 1)
+
+ BUILDER:SetPropertyGroup("Follow")
+ :GetSet("Follow", false, {description = "Whether the physics object should follow via SetPos. But it might clip in the world! seconds to arrive will be used for deciding the speed"})
+ :GetSet("PushFollow", false, {description = "Whether the physics object should try to follow via AddVelocity, to prevent phasing through walls. But it might get stuck in a corner!\n"..
+ "seconds to arrive, along with the extra distance if it's beyond the constrain sphere, will be used for deciding the speed"})
+ :GetSet("SecondsToArrive", 0.1)
+ :GetSet("MaxSpeed", 10000)
+ :GetSet("MaxAngular", 3600)
+ :GetSet("MaxSpeedDamp", 1000)
+ :GetSet("MaxAngularDamp", 1000)
+ :GetSet("DampFactor", 1)
+ BUILDER:SetPropertyGroup("Speeds")
+
+ :GetSet("ConstantVelocity", Vector(0,0,0))
+
+ BUILDER:SetPropertyGroup("Shape")
+ :GetSet("BoxScale",Vector(1,1,1))
+ :GetSet("Box", true)
+ :GetSet("Radius", 1)
+ :GetSet("SurfaceProperties", "default", {enums = physprop_enums})
+ :GetSet("Preview", false)
+ :GetSet("Mass", 100)
+
+ BUILDER:SetPropertyGroup("InitialVelocity")
+ :GetSet("AddOwnerSpeed", false)
+ :GetSet("InitialVelocityVector", Vector(0,0,0))
+ :GetSetPart("InitialVelocityPart")
+ :GetSet("OverrideInitialPosition", false, {description = "Whether the initial velocity part should be used as an initial position, otherwise it'll just be for the initial velocity's angle"})
- BUILDER:GetSet("MaxSpeed", 10000)
- BUILDER:GetSet("MaxAngular", 3600)
- BUILDER:GetSet("MaxSpeedDamp", 1000)
- BUILDER:GetSet("MaxAngularDamp", 1000)
- BUILDER:GetSet("DampFactor", 1)
-
- BUILDER:GetSet("ConstrainSphere", 0)
BUILDER:EndStorableVars()
local function IsInvalidParent(self)
@@ -55,6 +86,38 @@ function PART:SetMass(n)
end
end
+function PART:MeshDraw()
+ if not IsValid(self.phys) then return end
+
+ local mesh = (self.phys):GetMesh()
+ local drawmesh = Mesh()
+
+ if mesh == nil or not self.Box then
+ render.DrawWireframeSphere( self.phys:GetPos(), self.Radius, 10, 10, Color( 255, 255, 255 ) )
+ else
+ drawmesh:BuildFromTriangles(mesh)
+
+ render.SetMaterial( Material( "models/wireframe" ) )
+ local mat = Matrix()
+ mat:Translate(self.phys:GetPos())
+ mat:Rotate(self.phys:GetAngles())
+ cam.PushModelMatrix( mat )
+ drawmesh:Draw()
+ cam.PopModelMatrix()
+ end
+end
+
+function PART:SetPreview(b)
+ self.Preview = b
+ if self.Preview then
+ hook.Add("PostDrawTranslucentRenderables", "pac_physics_preview"..self.UniqueID, function()
+ self:MeshDraw()
+ end)
+ else
+ hook.Remove("PostDrawTranslucentRenderables", "pac_physics_preview"..self.UniqueID)
+ end
+end
+
function PART:SetRadius(n)
self.Radius = n
@@ -67,9 +130,9 @@ function PART:SetRadius(n)
ent:SetNoDraw(false)
if self.Box then
- ent:PhysicsInitBox(Vector(1,1,1) * -n, Vector(1,1,1) * n)
+ ent:PhysicsInitBox(self.BoxScale * -n, self.BoxScale * n, self.SurfaceProperties)
else
- ent:PhysicsInitSphere(n)
+ ent:PhysicsInitSphere(n, self.SurfaceProperties)
end
self.phys = ent:GetPhysicsObject()
@@ -79,6 +142,20 @@ function PART:SetRadius(n)
end
end
+function PART:SetSurfaceProperties(str)
+ self.SurfaceProperties = str
+ self:SetRadius(self.Radius) --refresh the physics
+end
+
+function PART:GetSurfacePropsTable() --to view info over in the properties
+ return util.GetSurfaceData(physprop_indices[self.SurfaceProperties])
+end
+
+function PART:SetBoxScale(vec)
+ self.BoxScale = vec
+ self:SetRadius(self.Radius) --refresh the physics
+end
+
function PART:SetGravity(b)
self.Gravity = b
@@ -121,25 +198,34 @@ function PART:OnThink()
if phys:IsValid() then
phys:Wake()
- if self.Follow then
- params.pos = self.Parent:GetWorldPosition()
- params.angle = self.Parent:GetWorldAngles()
+ if self.Follow or self.PushFollow then
+ if not self.PushFollow then
+ params.pos = self.Parent:GetWorldPosition()
+ params.angle = self.Parent:GetWorldAngles()
- params.secondstoarrive = math.max(self.SecondsToArrive, 0.0001)
- params.maxangular = self.MaxAngular
- params.maxangulardamp = self.MaxAngularDamp
- params.maxspeed = self.MaxSpeed
- params.maxspeeddamp = self.MaxSpeedDamp
- params.dampfactor = self.DampFactor
+ params.secondstoarrive = math.max(self.SecondsToArrive, 0.0001)
+ params.maxangular = self.MaxAngular
+ params.maxangulardamp = self.MaxAngularDamp
+ params.maxspeed = self.MaxSpeed
+ params.maxspeeddamp = self.MaxSpeedDamp
+ params.dampfactor = self.DampFactor
- params.teleportdistance = 0
-
- phys:ComputeShadowControl(params)
+ params.teleportdistance = 0
+ phys:ComputeShadowControl(params)
+ end
-- this is nicer i think
if self.ConstrainSphere ~= 0 and phys:GetPos():Distance(self.Parent:GetWorldPosition()) > self.ConstrainSphere then
- phys:SetPos(self.Parent:GetWorldPosition() + (self.Parent:GetWorldPosition() - phys:GetPos()):GetNormalized() * -self.ConstrainSphere)
+ if not self.PushFollow then
+ phys:SetPos(self.Parent:GetWorldPosition() + (self.Parent:GetWorldPosition() - phys:GetPos()):GetNormalized() * -self.ConstrainSphere)
+ --new push mode
+ else
+ local vec = (self.Parent:GetWorldPosition() - phys:GetPos())
+ local current_dist = vec:Length()
+ local extra_dist = current_dist - self.ConstrainSphere
+ phys:AddVelocity(0.5 * vec:GetNormalized() * extra_dist / math.Clamp(self.SecondsToArrive,0.05,10))
+ end
end
else
if self.ConstrainSphere ~= 0 then
@@ -162,13 +248,25 @@ function PART:OnUnParent(part)
timer.Simple(0, function() self:Disable() end)
end
-
function PART:OnShow()
timer.Simple(0, function() self:Enable() end)
end
function PART:OnHide()
+ if not self.Hide and not self:IsHidden() then
+ timer.Simple(0.4, function()
+ self:GetRootPart():OnShow()
+ self.Parent:OnShow()
+ for _,part in pairs(self:GetParent():GetChildrenList()) do
+ part:OnShow()
+
+ end
+ end)
+ return
+ end
timer.Simple(0, function() self:Disable() end)
+
+ hook.Remove("PostDrawTranslucentRenderables", "pac_physics_preview"..self.UniqueID)
end
function PART:Enable()
@@ -190,14 +288,60 @@ function PART:Enable()
end
self.disabled = false
+
+ if IsValid(self.InitialVelocityPart) then
+ if self.InitialVelocityPart.GetWorldPosition then
+ local local_vec, local_ang = self.InitialVelocityPart:GetDrawPosition()
+ local local_vec2 = self.InitialVelocityVector.x * local_ang:Forward() +
+ self.InitialVelocityVector.y * local_ang:Right() +
+ self.InitialVelocityVector.z * local_ang:Up()
+ self.phys:AddVelocity(local_vec2)
+ if self.OverrideInitialPosition then
+ self.phys:SetPos(local_vec)
+ end
+ else
+ self.phys:AddVelocity(self.InitialVelocityVector)
+ end
+ else
+ self.phys:AddVelocity(self.InitialVelocityVector)
+ end
+
+ if self.AddOwnerSpeed then
+ self.phys:AddVelocity(self:GetRootPart():GetOwner():GetVelocity())
+ end
+
+ timer.Simple(self.ThinkDelay, function() hook.Add("Tick", "pac_phys_repulsionthink"..self.UniqueID, function()
+ if not IsValid(self.phys) then hook.Remove("Tick", "pac_phys_repulsionthink"..self.UniqueID) return end
+ self.phys:AddVelocity(self.ConstantVelocity * RealFrameTime())
+
+ if self.Pushable then
+ local pushvec = Vector(0,0,0)
+ local pos = self.phys:GetPos()
+ local ents_tbl = ents.FindInSphere(pos, self.Radius)
+ local valid_phys_pushers = 0
+ for i,ent in pairs(ents_tbl) do
+ if ent.GetPhysicsObject or ent:IsPlayer() then
+ if ent:IsPlayer() or ent:GetClass() == "prop_physics" or ent:GetClass() == "prop_ragdoll" then
+ valid_phys_pushers = valid_phys_pushers + 1
+ pushvec = pushvec + (pos - ent:GetPos()):GetNormalized() * 20
+ end
+ end
+ end
+ if valid_phys_pushers > 0 then self.phys:AddVelocity(pushvec / valid_phys_pushers) end
+ end
+
+
+ end) end)
end
function PART:Disable()
+ hook.Remove("Tick", "pac_phys_repulsionthink"..self.UniqueID)
if IsInvalidParent(self) then return end
local part = self:GetParent()
local ent = part:GetOwner()
+
if ent:IsValid() then
-- SetNoDraw does not care of validity but PhysicsInit does?
ent:SetNoDraw(true)
diff --git a/lua/pac3/core/client/parts/player_config.lua b/lua/pac3/core/client/parts/player_config.lua
index 10a121e38..cdcc88508 100644
--- a/lua/pac3/core/client/parts/player_config.lua
+++ b/lua/pac3/core/client/parts/player_config.lua
@@ -29,6 +29,7 @@ BUILDER:SetPropertyGroup("generic")
BUILDER:SetPropertyGroup("behavior")
BUILDER:GetSet("MuteFootsteps", false)
+ BUILDER:GetSet("AnimationRate", 1)
BUILDER:SetPropertyGroup("death")
BUILDER:GetSet("FallApartOnDeath", false)
diff --git a/lua/pac3/core/client/parts/projectile.lua b/lua/pac3/core/client/parts/projectile.lua
index b5b35b4d8..5d461e46d 100644
--- a/lua/pac3/core/client/parts/projectile.lua
+++ b/lua/pac3/core/client/parts/projectile.lua
@@ -1,93 +1,124 @@
+local physprop_enums = {}
+local physprop_indices = {}
+for i=0,200,1 do
+ local name = util.GetSurfacePropName(i)
+ if name ~= "" then
+ physprop_enums[name] = name
+ physprop_indices[name] = i
+ end
+end
+
language.Add("pac_projectile", "Projectile")
+
+
local BUILDER, PART = pac.PartTemplate("base_movable")
PART.ClassName = "projectile"
-PART.Group = 'advanced'
-PART.Icon = 'icon16/bomb.png'
+PART.Group = {"advanced", "combat"}
+PART.Icon = "icon16/bomb.png"
+
+PART.ImplementsDoubleClickSpecified = true
BUILDER:StartStorableVars()
- BUILDER:GetSet("Speed", 1)
- BUILDER:GetSet("AddOwnerSpeed", false)
- BUILDER:GetSet("Damping", 0)
- BUILDER:GetSet("Gravity", true)
- BUILDER:GetSet("Collisions", true)
- BUILDER:GetSet("Sphere", false)
- BUILDER:GetSet("Radius", 1)
- BUILDER:GetSet("DamageRadius", 50)
- BUILDER:GetSet("LifeTime", 5)
- BUILDER:GetSet("AimDir", false)
- BUILDER:GetSet("Sticky", false)
- BUILDER:GetSet("Bounce", 0)
- BUILDER:GetSet("BulletImpact", false)
- BUILDER:GetSet("Damage", 0)
- BUILDER:GetSet("DamageType", "generic", {enums = {
- generic = 0, --generic damage
- crush = 1, --caused by physics interaction
- bullet = 2, --bullet damage
- slash = 4, --sharp objects, such as manhacks or other npcs attacks
- burn = 8, --damage from fire
- vehicle = 16, --hit by a vehicle
- fall = 32, --fall damage
- blast = 64, --explosion damage
- club = 128, --crowbar damage
- shock = 256, --electrical damage, shows smoke at the damage position
- sonic = 512, --sonic damage,used by the gargantua and houndeye npcs
- energybeam = 1024, --laser
- nevergib = 4096, --don't create gibs
- alwaysgib = 8192, --always create gibs
- drown = 16384, --drown damage
- paralyze = 32768, --same as dmg_poison
- nervegas = 65536, --neurotoxin damage
- poison = 131072, --poison damage
- acid = 1048576, --
- airboat = 33554432, --airboat gun damage
- blast_surface = 134217728, --this won't hurt the player underwater
- buckshot = 536870912, --the pellets fired from a shotgun
- direct = 268435456, --
- dissolve = 67108864, --forces the entity to dissolve on death
- drownrecover = 524288, --damage applied to the player to restore health after drowning
- physgun = 8388608, --damage done by the gravity gun
- plasma = 16777216, --
- prevent_physics_force = 2048, --
- radiation = 262144, --radiation
- removenoragdoll = 4194304, --don't create a ragdoll on death
- slowburn = 2097152, --
-
- explosion = -1, -- util.BlastDamage
- fire = -1, -- ent:Ignite(5)
-
- -- env_entity_dissolver
- dissolve_energy = 0,
- dissolve_heavy_electrical = 1,
- dissolve_light_electrical = 2,
- dissolve_core_effect = 3,
-
- heal = -1,
- armor = -1,
- }
- })
- BUILDER:GetSet("Spread", 0)
- BUILDER:GetSet("Delay", 0)
- BUILDER:GetSet("Maximum", 0)
- BUILDER:GetSet("Mass", 100)
- BUILDER:GetSet("Attract", 0)
- BUILDER:GetSet("AttractMode", "projectile_nearest", {enums = {
- hitpos = "hitpos",
- hitpos_radius = "hitpos_radius",
- closest_to_projectile = "closest_to_projectile",
- closest_to_hitpos = "closest_to_hitpos",
- }})
- BUILDER:GetSet("AttractRadius", 200)
- BUILDER:GetSetPart("OutfitPart")
- BUILDER:GetSet("Physical", false)
- BUILDER:GetSet("CollideWithOwner", false)
- BUILDER:GetSet("CollideWithSelf", false)
- BUILDER:GetSet("RemoveOnCollide", false)
+ BUILDER:SetPropertyGroup("Firing")
+ BUILDER:GetSet("Speed", 1)
+ BUILDER:GetSet("AddOwnerSpeed", false)
+ BUILDER:GetSet("Spread", 0)
+ BUILDER:GetSet("NumberProjectiles", 1)
+ BUILDER:GetSet("Delay", 0, {editor_clamp = {0,80}})
+ BUILDER:GetSet("Maximum", 0)
+ BUILDER:GetSet("RandomAngleVelocity", Vector(0,0,0))
+ BUILDER:GetSet("LocalAngleVelocity", Vector(0,0,0))
+ BUILDER:SetPropertyGroup("Physics")
+ BUILDER:GetSet("Freeze", false, {description = "frozen like physgun"})
+ BUILDER:GetSet("Mass", 100, {editor_clamp = {0,50000}}) --there's actually a 50k limit
+ BUILDER:GetSet("ImpactSounds", true, {description = "allow physics impact sounds, applies to physical projectiles"})
+ BUILDER:GetSet("SurfaceProperties", "default", {enums = physprop_enums})
+ BUILDER:GetSet("RescalePhysMesh", false, {description = "experimental! tries to scale the collide mesh by the radius! Stay within small numbers! 1 radius should be associated with a full-size model"})
+ BUILDER:GetSet("OverridePhysMesh", false, {description = "experimental! tries to redefine the projectile's model to change the physics mesh"})
+ BUILDER:GetSet("FallbackSurfpropModel", "models/props_junk/PopCan01a.mdl", {editor_friendly = "collide mesh", editor_panel = "model"})
+ BUILDER:GetSet("Damping", 0)
+ BUILDER:GetSet("Gravity", true)
+ BUILDER:GetSet("Collisions", true)
+ BUILDER:GetSet("Sphere", false)
+ BUILDER:GetSet("Radius", 1, {editor_panel = "projectile_radii"})
+ BUILDER:GetSet("Bounce", 0, {editor_clamp = {-160,160}})
+ BUILDER:GetSet("Sticky", false)
+ BUILDER:GetSet("CollideWithOwner", false)
+ BUILDER:GetSet("CollideWithSelf", false)
+ BUILDER:SetPropertyGroup("Appearance")
+ BUILDER:GetSetPart("OutfitPart")
+ BUILDER:GetSet("RemoveOnHide", false)
+ BUILDER:GetSet("AimDir", false)
+ BUILDER:GetSet("DrawShadow", true)
+ BUILDER:SetPropertyGroup("ActiveBehavior")
+ BUILDER:GetSet("Physical", false)
+ BUILDER:GetSet("DamageRadius", 50, {editor_panel = "projectile_radii"})
+ BUILDER:GetSet("LifeTime", 5)
+ BUILDER:GetSet("RemoveOnCollide", false)
+ BUILDER:GetSet("BulletImpact", false)
+ BUILDER:GetSet("Damage", 0)
+ BUILDER:GetSet("DamageType", "generic", {enums = {
+ generic = 0, --generic damage
+ crush = 1, --caused by physics interaction
+ bullet = 2, --bullet damage
+ slash = 4, --sharp objects, such as manhacks or other npcs attacks
+ burn = 8, --damage from fire
+ vehicle = 16, --hit by a vehicle
+ fall = 32, --fall damage
+ blast = 64, --explosion damage
+ club = 128, --crowbar damage
+ shock = 256, --electrical damage, shows smoke at the damage position
+ sonic = 512, --sonic damage,used by the gargantua and houndeye npcs
+ energybeam = 1024, --laser
+ nevergib = 4096, --don't create gibs
+ alwaysgib = 8192, --always create gibs
+ drown = 16384, --drown damage
+ paralyze = 32768, --same as dmg_poison
+ nervegas = 65536, --neurotoxin damage
+ poison = 131072, --poison damage
+ acid = 1048576, --
+ airboat = 33554432, --airboat gun damage
+ blast_surface = 134217728, --this won't hurt the player underwater
+ buckshot = 536870912, --the pellets fired from a shotgun
+ direct = 268435456, --
+ dissolve = 67108864, --forces the entity to dissolve on death
+ drownrecover = 524288, --damage applied to the player to restore health after drowning
+ physgun = 8388608, --damage done by the gravity gun
+ plasma = 16777216, --
+ prevent_physics_force = 2048, --
+ radiation = 262144, --radiation
+ removenoragdoll = 4194304, --don't create a ragdoll on death
+ slowburn = 2097152, --
+
+ explosion = -1, -- util.BlastDamage
+ fire = -1, -- ent:Ignite(5)
+
+ -- env_entity_dissolver
+ dissolve_energy = 0,
+ dissolve_heavy_electrical = 1,
+ dissolve_light_electrical = 2,
+ dissolve_core_effect = 3,
+
+ heal = -1,
+ armor = -1,
+ }
+ })
+ BUILDER:GetSet("Attract", 0, {editor_friendly = "attract force"})
+ BUILDER:GetSet("AttractMode", "closest_to_projectile", {enums = {
+ hitpos = "hitpos",
+ hitpos_radius = "hitpos_radius",
+ closest_to_projectile = "closest_to_projectile",
+ closest_to_hitpos = "closest_to_hitpos",
+ }})
+ BUILDER:GetSet("AttractRadius", 200)
+
BUILDER:EndStorableVars()
PART.Translucent = false
+
function PART:OnShow(from_rendering)
if not from_rendering then
-- TODO:
@@ -103,11 +134,20 @@ function PART:OnShow(from_rendering)
part:Draw("opaque")
end
end
- self:Shoot(self:GetDrawPosition())
+ if self.NumberProjectiles <= 0 then self.NumberProjectiles = 0 end
+ if self.NumberProjectiles <= 50 then
+ local pos,ang = self:GetDrawPosition()
+ self:Shoot(pos,ang,self.NumberProjectiles)
+ else chat.AddText(Color(255,0,0),"[PAC3] Trying to spawn too many projectiles! The limit is " .. 50) end
end
end
-function PART:AttachToEntity(ent)
+function PART:GetSurfacePropsTable() --to view info over in the properties
+ return util.GetSurfaceData(physprop_indices[self.SurfaceProperties])
+end
+
+local_projectiles = {}
+function PART:AttachToEntity(ent, physical)
if not self.OutfitPart:IsValid() then return false end
ent.pac_draw_distance = 0
@@ -140,28 +180,130 @@ function PART:AttachToEntity(ent)
end
ent.pac_projectile_part = group
- ent.pac_projectile = self
+ ent.pac_projectile = self --that's just the launcher though
+ if not physical then local_projectiles[group] = ent end
return true
end
local enable = CreateClientConVar("pac_sv_projectiles", 0, true)
-function PART:Shoot(pos, ang)
+local damage_ids = {
+ generic = 0, --generic damage
+ crush = 1, --caused by physics interaction
+ bullet = 2, --bullet damage
+ slash = 3, --sharp objects, such as manhacks or other npcs attacks
+ burn = 4, --damage from fire
+ vehicle = 5, --hit by a vehicle
+ fall = 6, --fall damage
+ blast = 7, --explosion damage
+ club = 8, --crowbar damage
+ shock = 9, --electrical damage, shows smoke at the damage position
+ sonic = 10, --sonic damage,used by the gargantua and houndeye npcs
+ energybeam = 11, --laser
+ nevergib = 12, --don't create gibs
+ alwaysgib = 13, --always create gibs
+ drown = 14, --drown damage
+ paralyze = 15, --same as dmg_poison
+ nervegas = 16, --neurotoxin damage
+ poison = 17, --poison damage
+ acid = 18, --
+ airboat = 19, --airboat gun damage
+ blast_surface = 20, --this won't hurt the player underwater
+ buckshot = 21, --the pellets fired from a shotgun
+ direct = 22, --
+ dissolve = 23, --forces the entity to dissolve on death
+ drownrecover = 24, --damage applied to the player to restore health after drowning
+ physgun = 25, --damage done by the gravity gun
+ plasma = 26, --
+ prevent_physics_force = 27, --
+ radiation = 28, --radiation
+ removenoragdoll = 29, --don't create a ragdoll on death
+ slowburn = 30, --
+
+ explosion = 31, -- ent:Ignite(5)
+ fire = 32, -- ent:Ignite(5)
+
+ -- env_entity_dissolver
+ dissolve_energy = 33,
+ dissolve_heavy_electrical = 34,
+ dissolve_light_electrical = 35,
+ dissolve_core_effect = 36,
+
+ heal = 37,
+ armor = 38,
+}
+local attract_ids = {
+ hitpos = 0,
+ hitpos_radius = 1,
+ closest_to_projectile = 2,
+ closest_to_hitpos = 3,
+}
+function PART:Shoot(pos, ang, multi_projectile_count)
local physics = self.Physical
+ local multi_projectile_count = multi_projectile_count or 1
if physics then
if pac.LocalPlayer ~= self:GetPlayerOwner() then return end
local tbl = {}
- for key in pairs(self:GetStorableVars()) do
- tbl[key] = self[key]
- end
- net.Start("pac_projectile")
+ net.Start("pac_projectile",true)
+ net.WriteUInt(multi_projectile_count,7)
net.WriteVector(pos)
net.WriteAngle(ang)
- net.WriteTable(tbl)
+
+ --bools
+ net.WriteBool(self.Sphere)
+ net.WriteBool(self.RemoveOnCollide)
+ net.WriteBool(self.CollideWithOwner)
+ net.WriteBool(self.RemoveOnHide)
+ net.WriteBool(self.RescalePhysMesh)
+ net.WriteBool(self.OverridePhysMesh)
+ net.WriteBool(self.Gravity)
+ net.WriteBool(self.AddOwnerSpeed)
+ net.WriteBool(self.Collisions)
+ net.WriteBool(self.CollideWithSelf)
+ net.WriteBool(self.AimDir)
+ net.WriteBool(self.DrawShadow)
+ net.WriteBool(self.Sticky)
+ net.WriteBool(self.BulletImpact)
+ net.WriteBool(self.Freeze)
+ net.WriteBool(self.ImpactSounds)
+
+ --vectors
+ net.WriteVector(self.RandomAngleVelocity)
+ net.WriteVector(self.LocalAngleVelocity)
+
+ --strings
+ net.WriteString(self.OverridePhysMesh and string.sub(string.gsub(self.FallbackSurfpropModel, "^models/", ""),1,150) or "") --custom model is an unavoidable string
+ net.WriteString(string.sub(self.UniqueID,1,12)) --long string but we can probably truncate it
+ net.WriteUInt(physprop_indices[self.SurfaceProperties] or 0,10)
+ net.WriteUInt(damage_ids[self.DamageType] or 0,7)
+ net.WriteUInt(attract_ids[self.AttractMode] or 2,3)
+
+ --numbers
+ local using_decimal = (self.Radius % 1 ~= 0) and self.RescalePhysMesh
+ net.WriteBool(using_decimal)
+ if using_decimal then
+ net.WriteFloat(self.Radius)
+ else
+ net.WriteUInt(self.Radius,12)
+ end
+
+ net.WriteUInt(self.DamageRadius,12)
+ net.WriteUInt(self.Damage,24)
+ net.WriteInt(1000*self.Speed,18)
+ net.WriteUInt(self.Maximum,7)
+ net.WriteUInt(100*self.LifeTime,14) --might need decimals
+ net.WriteUInt(100*self.Delay,13) --might need decimals
+ net.WriteUInt(self.Mass,16)
+ net.WriteInt(100*self.Spread,10)
+ net.WriteInt(100*self.Damping,20) --might need decimals
+ net.WriteInt(self.Attract,14)
+ net.WriteUInt(self.AttractRadius,10)
+ net.WriteInt(100*self.Bounce,15) --might need decimals
+
net.SendToServer()
else
self.projectiles = self.projectiles or {}
@@ -190,7 +332,7 @@ function PART:Shoot(pos, ang)
if not self:IsValid() then return end
- local ent = pac.CreateEntity("models/props_junk/popcan01a.mdl")
+ local ent = pac.CreateEntity(self.FallbackSurfpropModel)
if not ent:IsValid() then return end
local idx = table.insert(self.projectiles, ent)
@@ -232,9 +374,18 @@ function PART:Shoot(pos, ang)
ent:SetCollisionGroup(COLLISION_GROUP_PROJECTILE)
if self.Sphere then
- ent:PhysicsInitSphere(math.Clamp(self.Radius, 1, 30))
+ ent:PhysicsInitSphere(math.Clamp(self.Radius, 1, 500), self.SurfaceProperties)
else
- ent:PhysicsInitBox(Vector(1,1,1) * - math.Clamp(self.Radius, 1, 30), Vector(1,1,1) * math.Clamp(self.Radius, 1, 30))
+ ent:PhysicsInitBox(Vector(1,1,1) * - math.Clamp(self.Radius, 1, 500), Vector(1,1,1) * math.Clamp(self.Radius, 1, 500), self.SurfaceProperties)
+ if self.OverridePhysMesh then
+ local valid_fallback = util.IsValidModel( self.FallbackSurfpropModel ) and not IsUselessModel(self.FallbackSurfpropModel)
+ ent:PhysicsInitBox(Vector(1,1,1) * - math.Clamp(self.Radius, 1, 500), Vector(1,1,1) * math.Clamp(self.Radius, 1, 500), self.FallbackSurfpropModel)
+ if self.OverridePhysMesh and valid_fallback then
+ ent:SetModel(self.FallbackSurfpropModel)
+ ent:PhysicsInit(SOLID_VPHYSICS)
+ ent:GetPhysicsObject():SetMaterial(self.SurfaceProperties)
+ end
+ end
end
ent.RenderOverride = function()
@@ -268,7 +419,8 @@ function PART:Shoot(pos, ang)
ent:SetCollisionGroup(COLLISION_GROUP_PROJECTILE)
- if self:AttachToEntity(ent) then
+ if self:AttachToEntity(ent, false) then
+
timer.Simple(math.Clamp(self.LifeTime, 0, 10), function()
if ent:IsValid() then
if ent.pac_projectile_part and ent.pac_projectile_part:IsValid() then
@@ -280,26 +432,104 @@ function PART:Shoot(pos, ang)
end)
end
end)
+
end
+
+
end
if self.Delay == 0 then
- spawn()
+ for i = multi_projectile_count,1,-1 do
+ spawn()
+ end
else
timer.Simple(self.Delay, spawn)
end
end
end
-function PART:OnRemove()
- if not self.Physical and self.projectiles then
- for key, ent in pairs(self.projectiles) do
- SafeRemoveEntity(ent)
- end
+function PART:OnDoubleClickSpecified()
+ self:Shoot()
+end
+
+function PART:SetRadius(val)
+ self.Radius = val
+ local sv_dist = GetConVar("pac_sv_projectile_max_phys_radius"):GetInt()
+ if self.Radius > sv_dist then
+ self:SetInfo("Your radius is beyond the server's maximum permitted! Server max is " .. sv_dist)
+ else
+ self:SetInfo(nil)
+ end
+end
+
+function PART:SetDamageRadius(val)
+ self.DamageRadius = val
+ local sv_dist = GetConVar("pac_sv_projectile_max_damage_radius"):GetInt()
+ if self.DamageRadius > sv_dist then
+ self:SetInfo("Your damage radius is beyond the server's maximum permitted! Server max is " .. sv_dist)
+ else
+ self:SetInfo(nil)
+ end
+end
+
+function PART:SetAttractRadius(val)
+ self.AttractRadius = val
+ local sv_dist = GetConVar("pac_sv_projectile_max_attract_radius"):GetInt()
+ if self.AttractRadius > sv_dist then
+ self:SetInfo("Your attract radius is beyond the server's maximum permitted! Server max is " .. sv_dist)
+ else
+ self:SetInfo(nil)
+ end
+end
+
+function PART:SetSpeed(val)
+ self.Speed = val
+ local sv_max = GetConVar("pac_sv_projectile_max_speed"):GetInt()
+ if self.Speed > sv_max then
+ self:SetInfo("Your speed is beyond the server's maximum permitted! Server max is " .. sv_max)
+ else
+ self:SetInfo(nil)
+ end
+end
+
+function PART:SetMass(val)
+ self.Mass = val
+ local sv_max = GetConVar("pac_sv_projectile_max_mass"):GetInt()
+ if self.Mass > sv_max then
+ self:SetInfo("Your mass is beyond the server's maximum permitted! Server max is " .. sv_max)
+ elseif val > 50000 then
+ self:SetInfo("The game has a maximum of 50k mass")
+ else
+ self:SetInfo(nil)
+ end
+end
- self.projectiles = {}
+function PART:SetDamage(val)
+ self.Damage = val
+ local sv_max = GetConVar("pac_sv_damage_zone_max_damage"):GetInt()
+ if self.Damage > sv_max then
+ self:SetInfo("Your damage is beyond the server's maximum permitted! Server max is " .. sv_max)
+ else
+ self:SetInfo(nil)
end
end
+
+pac.AddHook("Think", "pac_cleanup_CS_projectiles", function()
+ for rootpart,ent in pairs(local_projectiles) do
+ if ent.pac_projectile_part == rootpart then
+ local tbl = ent.pac_projectile_part:GetChildren()
+ local partchild = tbl[next(tbl)] --ent.pac_projectile_part is the root group, but outfit part is the first child
+
+ if IsValid(partchild) then
+ if partchild:IsHidden() then
+ SafeRemoveEntity(ent)
+ end
+ end
+ end
+ end
+
+end)
+
--[[
function PART:OnHide()
if self.RemoveOnHide then
@@ -307,10 +537,25 @@ function PART:OnHide()
end
end
]]
+
+--[[if ent.pac_projectile_part then
+ local partchild = next(ent.pac_projectile_part:GetChildren()) --ent.pac_projectile_part is the root group, but outfit part is the first child
+ if IsValid(part) then
+ if partchild:IsHidden() then
+ if ent.pac_projectile.RemoveOnHide then
+ net.Start("pac_projectile_remove")
+ net.WriteInt(data.ent_id)
+ net.SendToServer()
+ end
+ end
+ end
+end]]
+
do -- physical
local Entity = Entity
local projectiles = {}
pac.AddHook("Think", "pac_projectile", function()
+
for key, data in pairs(projectiles) do
if not data.ply:IsValid() then
projectiles[key] = nil
@@ -320,12 +565,13 @@ do -- physical
local ent = Entity(data.ent_id)
if ent:IsValid() and ent:GetClass() == "pac_projectile" then
- local part = pac.GetPartFromUniqueID(pac.Hash(data.ply), data.partuid)
+ local part = pac.FindPartByPartialUniqueID(pac.Hash(data.ply), data.partuid)
if part:IsValid() and part:GetPlayerOwner() == data.ply then
- part:AttachToEntity(ent)
+ part:AttachToEntity(ent, true)
end
projectiles[key] = nil
end
+
::CONTINUE::
end
end)
@@ -334,10 +580,32 @@ do -- physical
local ply = net.ReadEntity()
local ent_id = net.ReadInt(16)
local partuid = net.ReadString()
+ local surfprop = net.ReadString()
if ply:IsValid() then
table.insert(projectiles, {ply = ply, ent_id = ent_id, partuid = partuid})
end
+
+ local ent = Entity(ent_id)
+
+ ent.Think = function()
+ if ent.pac_projectile_part then
+ local tbl = ent.pac_projectile_part:GetChildren()
+ local partchild = tbl[next(tbl)] --ent.pac_projectile_part is the root group, but outfit part is the first child
+ if IsValid(partchild) then
+ if partchild:IsHidden() then
+ if ent.pac_projectile.RemoveOnHide and not ent.markedforremove then
+ ent.markedforremove = true
+ net.Start("pac_projectile_remove")
+ net.WriteInt(ent_id, 16)
+ net.SendToServer()
+
+ end
+ end
+ end
+
+ end
+ end
end)
end
diff --git a/lua/pac3/core/client/parts/proxy.lua b/lua/pac3/core/client/parts/proxy.lua
index b111bd342..eddbef0b6 100644
--- a/lua/pac3/core/client/parts/proxy.lua
+++ b/lua/pac3/core/client/parts/proxy.lua
@@ -24,18 +24,19 @@ BUILDER:StartStorableVars()
end
return tbl
- end})
+ end, description = "What property of the target part should be changed.\nYou don't need to follow the list builder if you know what you're doing with target parts."})
BUILDER:GetSet("RootOwner", false)
- BUILDER:GetSetPart("TargetPart")
+ BUILDER:GetSetPart("TargetPart", {description = "send output to an external part. supports name and uid"})
+ BUILDER:GetSet("MultipleTargetParts", "", {description = "send output to multiple external partss.\npaste multiple UIDs or names here, separated by semicolons. With bulk select, you can select parts and right click to get that done quickly.."})
BUILDER:GetSetPart("OutputTargetPart", {hide_in_editor = true})
BUILDER:GetSet("AffectChildren", false)
- BUILDER:GetSet("Expression", "")
+ BUILDER:GetSet("Expression", "", {description = "write math here. hit F1 for a tutorial or right click for examples.", editor_panel = "code_proxy"})
BUILDER:SetPropertyGroup("easy setup")
- BUILDER:GetSet("Input", "time", {enums = function(part) return part.Inputs end})
- BUILDER:GetSet("Function", "sin", {enums = function(part) return part.Functions end})
- BUILDER:GetSet("Axis", "")
+ BUILDER:GetSet("Input", "time", {enums = function(part) return part.Inputs end, description = "base (inner) function for easy setup\nin sin(time()) it is time"})
+ BUILDER:GetSet("Function", "sin", {enums = function(part) return part.Functions end, description = "processing (outer) function for easy setup.\nin sin(time()) it is sin"})
+ BUILDER:GetSet("Axis", "", {description = "The direction where the output ends up.\nx,y,z for vectors, p,y,r or r,g,b for colors\nIf you provide an expression with vector notation content, it will expand to the next axes. for example \"0,1,2\" on y will put 0 on y and 1 on z, 2 will overflow to nowhere."})
BUILDER:GetSet("Min", 0)
BUILDER:GetSet("Max", 1)
BUILDER:GetSet("Offset", 0)
@@ -44,12 +45,20 @@ BUILDER:StartStorableVars()
BUILDER:GetSet("Pow", 1)
BUILDER:SetPropertyGroup("behavior")
- BUILDER:GetSet("Additive", false)
- BUILDER:GetSet("PlayerAngles", false)
- BUILDER:GetSet("ZeroEyePitch", false)
- BUILDER:GetSet("ResetVelocitiesOnHide", true)
+ BUILDER:GetSet("Additive", false, {description = "This means that every computation frame, the proxy will add its output to its current stored memory. This can quickly get out of control if you don't know what you're doing! This is like using the feedback() function"})
+ BUILDER:GetSet("PlayerAngles", false, {description = "For some functions/inputs (eye angles, owner velocity increases, aim length) it will choose between the owner entity's Angles or EyeAngles. Unsure of whether this makes a difference."})
+ BUILDER:GetSet("ZeroEyePitch", false, {description = "For some functions/inputs (eye angles, owner velocity increases, aim length) it will force the angle to be horizon level."})
+ BUILDER:GetSet("ResetVelocitiesOnHide", true, {description = "Because velocity calculators use smoothing that makes the output converge toward a crude rolling average, it might matter whether you want to get a clean slate readout.\n(VelocityRoughness is how close to the snapshots it will be. Lower means smoother but delayed. Higher means less smoothing but it might overshoot and be inaccurate because of frame time works and varies)"})
BUILDER:GetSet("VelocityRoughness", 10)
-
+ BUILDER:GetSet("PreviewOutput", false, {description = "Previews the proxy's output (for yourself) next to the nearest owner entity in the game"})
+
+ BUILDER:SetPropertyGroup("extra expressions")
+ BUILDER:GetSet("ExpressionOnHide", "", {description = "Math to apply once, when the proxy is hidden. It computes once, so it will not move.", editor_panel = "code_proxy"})
+ BUILDER:GetSet("Extra1", "", {description = "Write extra math here.\nIt computes before the main expression and can be accessed from the main expression as extra1() or var1() to save space, or by another proxy as extra1(\"uid or name\") or var1(\"uid or name\")", editor_panel = "code_proxy"})
+ BUILDER:GetSet("Extra2", "", {description = "Write extra math here.\nIt computes before the main expression and can be accessed from the main expression as extra2() or var2() to save space, or by another proxy as extra2(\"uid or name\") or var2(\"uid or name\")", editor_panel = "code_proxy"})
+ BUILDER:GetSet("Extra3", "", {description = "Write extra math here.\nIt computes before the main expression and can be accessed from the main expression as extra3() or var3() to save space, or by another proxy as extra3(\"uid or name\") or var3(\"uid or name\")", editor_panel = "code_proxy"})
+ BUILDER:GetSet("Extra4", "", {description = "Write extra math here.\nIt computes before the main expression and can be accessed from the main expression as extra4() or var4() to save space, or by another proxy as extra4(\"uid or name\") or var4(\"uid or name\")", editor_panel = "code_proxy"})
+ BUILDER:GetSet("Extra5", "", {description = "Write extra math here.\nIt computes before the main expression and can be accessed from the main expression as extra5() or var5() to save space, or by another proxy as extra5(\"uid or name\") or var5(\"uid or name\")", editor_panel = "code_proxy"})
BUILDER:EndStorableVars()
-- redirect
@@ -93,6 +102,69 @@ function PART:GetTarget()
return self:GetParent()
end
+function PART:GetOrFindCachedPart(uid_or_name)
+ local part = nil
+ self.erroring_cached_parts = {}
+ self.found_cached_parts = self.found_cached_parts or {}
+ if self.found_cached_parts[uid_or_name] then self.erroring_cached_parts[uid_or_name] = nil return self.found_cached_parts[uid_or_name] end
+ if self.erroring_cached_parts[uid_or_name] then return end
+
+ local owner = self:GetPlayerOwner()
+ part = pac.GetPartFromUniqueID(pac.Hash(owner), uid_or_name) or pac.FindPartByPartialUniqueID(pac.Hash(owner), uid_or_name)
+ if not part:IsValid() then
+ part = pac.FindPartByName(pac.Hash(owner), uid_or_name, self)
+ else
+ self.found_cached_parts[uid_or_name] = part
+ return part
+ end
+ if not part:IsValid() then
+ self.erroring_cached_parts[uid_or_name] = true
+ else
+ self.found_cached_parts[uid_or_name] = part
+ return part
+ end
+ return part
+end
+
+function PART:SetMultipleTargetParts(str)
+ self.MultipleTargetParts = str
+ self.MultiTargetPart = {}
+ if str == "" then self.MultiTargetPart = nil self.ExtraHermites = nil return end
+ if not string.find(str, ";") then
+ local part = self:GetOrFindCachedPart(str)
+ if IsValid(part) then
+ self:SetTargetPart(part)
+ self.MultipleTargetParts = ""
+ else
+ timer.Simple(3, function()
+ local part = self:GetOrFindCachedPart(str)
+ if part then
+ self:SetTargetPart(part)
+ self.MultipleTargetParts = ""
+ end
+ end)
+ end
+ self.MultiTargetPart = nil
+ else
+ self:SetTargetPart()
+ self.MultiTargetPart = {}
+ self.ExtraHermites = {}
+ local uid_splits = string.Split(str, ";")
+ for i,uid2 in ipairs(uid_splits) do
+ local part = self:GetOrFindCachedPart(uid2)
+ if not IsValid(part) then
+ timer.Simple(3, function()
+ local part = self:GetOrFindCachedPart(uid2)
+ if part then table.insert(self.MultiTargetPart, part) table.insert(self.ExtraHermites, part) end
+ end)
+ else table.insert(self.MultiTargetPart, part) table.insert(self.ExtraHermites, part) end
+ end
+ self.ExtraHermites_Property = "MultipleTargetParts"
+ end
+
+end
+
+
function PART:SetVariableName(str)
self.VariableName = str
end
@@ -124,8 +196,17 @@ end
function PART:Initialize()
self.vec_additive = {}
self.next_vel_calc = 0
+ self.invalid_parts_in_expression = {}
+ if self:GetPlayerOwner() == pac.LocalPlayer then
+ self.errors_override = true
+ timer.Simple(5, function() self.errors_override = false end) --initialize hack to stop erroring when referenced parts aren't created yet but will be created shortly
+ end
+
end
+PART.Tutorials = include("pac3/editor/client/proxy_function_tutorials.lua")
+
+
PART.Functions =
{
none = function(n) return n end,
@@ -219,6 +300,20 @@ PART.Inputs.property = function(self, property_name, field)
return 0
end
+PART.Inputs.polynomial = function(self, x, ...)
+ x = x or 1
+ local total = 0
+ local args = { ... }
+
+ pow = 0
+ for _, coefficient in ipairs(args) do
+ total = total + coefficient*math.pow(x, pow)
+ pow = pow + 1
+ end
+ return total
+
+end
+
PART.Inputs.owner_position = function(self)
local owner = get_owner(self)
@@ -322,6 +417,8 @@ PART.Inputs.lerp = function(self, m, a, b)
return (b - a) * m + a
end
+--I'll be reusing these on sample and hold / drift
+local ease_aliases = {}
for ease,f in pairs(math.ease) do
if string.find(ease,"In") or string.find(ease,"Out") then
local f2 = function(self, frac, min, max)
@@ -332,8 +429,47 @@ for ease,f in pairs(math.ease) do
PART.Inputs["ease"..ease] = f2
PART.Inputs["ease_"..ease] = f2
PART.Inputs[ease] = f2
+ ease_aliases[ease] = ease
+ ease_aliases["ease"..ease] = ease
+ ease_aliases["ease_"..ease] = ease
+ end
+end
+
+PART.Inputs.sample_and_hold = function(self, seed, duration, min, max, ease)
+ if not seed then self:SetInfo("sample_and_hold's arguments are (seed, duration, min, max, ease)\nease is a string like \"linear\" \"InSine\" or \"InOutElastic\"") end
+ seed = seed or 0
+
+ min = min or 0
+ max = max or 1
+
+ duration = duration or 1
+ if duration == 0 then return min + math.random()*(max-min) end
+
+ self.samplehold = self.samplehold or {}
+ self.samplehold_prev = self.samplehold_prev or {}
+ self.samplehold_prev[seed] = self.samplehold_prev[seed] or {value = min, refresh = CurTime()}
+ self.samplehold[seed] = self.samplehold[seed] or {value = min + math.random()*(max-min), refresh = CurTime() + duration}
+
+ local prev = self.samplehold_prev[seed].value
+ local frac = 1 - (self.samplehold[seed].refresh - CurTime()) / duration
+ local delta = self.samplehold[seed].value - prev
+
+ if CurTime() > self.samplehold[seed].refresh then
+ self.samplehold_prev[seed] = self.samplehold[seed]
+ self.samplehold[seed] = {value = min + math.random()*(max-min), refresh = CurTime() + duration}
+ end
+ if not ease then
+ return self.samplehold[seed].value
+ elseif ease == "lin" or ease == "linear" then
+ return prev + frac * delta
+ else
+ local eased_frac = math.ease[ease_aliases[ease]] and math.ease[ease_aliases[ease]](frac) or 1
+ return prev + eased_frac*delta
end
end
+PART.Inputs.samplehold = PART.Inputs.sample_and_hold
+PART.Inputs.random_drift = PART.Inputs.sample_and_hold
+PART.Inputs.drift = PART.Inputs.sample_and_hold
PART.Inputs.timeex = function(s)
s.time = s.time or pac.RealTime
@@ -342,34 +478,344 @@ PART.Inputs.timeex = function(s)
end
PART.Inputs.part_distance = function(self, uid1, uid2)
- if not uid1 or not uid2 then return 0 end
+ if not uid1 then return 0 end
+ local PartA = self:GetOrFindCachedPart(uid1)
+ local PartB
+ if not uid2 then
+ PartB = self:GetParent()
+ else
+ PartB = self:GetOrFindCachedPart(uid2)
+ end
- local PartA = pac.GetPartFromUniqueID(pac.Hash(pac.LocalPlayer), uid1)
- if not PartA:IsValid() then PartA = pac.FindPartByName(pac.Hash(pac.LocalPlayer), uid1, self) end
+ if not IsValid(PartB) then
+ if uid2 then
+ --second argument exists and failed to find anything, ERROR
+ self.invalid_parts_in_expression[uid2] = "invalid argument " .. uid2 .. " in part_distance"
+ end
+ end
- local PartB = pac.GetPartFromUniqueID(pac.Hash(pac.LocalPlayer), uid2)
- if not PartB:IsValid() then PartB = pac.FindPartByName(pac.Hash(pac.LocalPlayer), uid2, self) end
+ if not IsValid(PartA) then --first argument exists and failed to find anything, ERROR
+ self.invalid_parts_in_expression[uid1] = "invalid argument " .. uid1 .. " in part_distance"
+ end
- if not PartA:IsValid() or not PartB:IsValid() then return 0 end
+ if not IsValid(PartA) or not IsValid(PartB) then return 0 end
+ if not PartA.Position or not PartB.Position then return 0 end
+ self.valid_parts_in_expression[PartA] = PartA
+ self.valid_parts_in_expression[PartB] = PartB
return (PartB:GetWorldPosition() - PartA:GetWorldPosition()):Length()
end
+PART.Inputs.sum = function(self, ...)
+ sum = 0
+ local args = { ... }
+ if not args[1] then return 0 end
+ for i=1,#args,1 do
+ sum = sum + args[i]
+ end
+ return sum
+end
+
+PART.Inputs.product = function(self, ...)
+ sum = 1
+ local args = { ... }
+ if not args[1] then return 0 end
+ for i=1,#args,1 do
+ sum = sum * args[i]
+ end
+ return sum
+end
+
+PART.Inputs.average = function(self, ...)
+ sum = 0
+ local args = { ... } if isvector(args[1]) then sum = vector_origin end
+ if not args[1] then return 0 end
+ for i=1,#args,1 do
+ sum = sum + args[i]
+ end
+ return sum / #args
+end
+PART.Inputs.mean = PART.Inputs.average
+
+PART.Inputs.median = function(self, ...)
+ sum = 0
+ local args = { ... } if isvector(args[1]) then sum = vector_origin end
+ if not args[1] then return 0 end
+ table.sort(args)
+ local count = #args
+ if (count % 2) == 1 then
+ return args[math.floor(count/2) + 1]
+ else
+ return (args[count/2] + args[count/2 + 1]) / 2
+ end
+ return sum
+end
+
+
+PART.Inputs.part_pos_x = function(self, uid1)
+ local PartA
+ if not uid1 then --no argument, take parent
+ PartA = self:GetParent()
+ return PartA:GetWorldPosition().x
+ else
+ PartA = self:GetOrFindCachedPart(uid1)
+ end
+
+ if not IsValid(PartA) and uid1 then --first argument exists and failed to find anything, ERROR
+ self.invalid_parts_in_expression[uid1] = "invalid argument " .. uid1 .. " in part_pos_x"
+ end
+
+ if not IsValid(PartA) then return 0 end
+ if not PartA.Position then return 0 end
+ self.valid_parts_in_expression[PartA] = PartA
+ return PartA:GetWorldPosition().x
+end
+
+PART.Inputs.part_pos_y = function(self, uid1)
+ local PartA
+ if not uid1 then --no argument, take parent
+ PartA = self:GetParent()
+ return PartA:GetWorldPosition().y
+ else
+ PartA = self:GetOrFindCachedPart(uid1)
+ end
+
+ if not IsValid(PartA) and uid1 then --first argument exists and failed to find anything, ERROR
+ self.invalid_parts_in_expression[uid1] = "invalid argument " .. uid1 .. " in part_pos_y"
+ end
+
+ if not IsValid(PartA) then return 0 end
+ if not PartA.Position then return 0 end
+ self.valid_parts_in_expression[PartA] = PartA
+ return PartA:GetWorldPosition().y
+end
+
+PART.Inputs.part_pos_z = function(self, uid1)
+ local PartA
+ if not uid1 then --no argument, take parent
+ PartA = self:GetParent()
+ return PartA:GetWorldPosition().z
+ else
+ PartA = self:GetOrFindCachedPart(uid1)
+ end
+
+ if not IsValid(PartA) and uid1 then --first argument exists and failed to find anything, ERROR
+ self.invalid_parts_in_expression[uid1] = "invalid argument " .. uid1 .. " in part_pos_z"
+ end
+
+ if not IsValid(PartA) then return 0 end
+ if not PartA.Position then return 0 end
+ self.valid_parts_in_expression[PartA] = PartA
+ return PartA:GetWorldPosition().z
+end
+
+PART.Inputs.delta_x = function(self, uid1, uid2)
+ if not uid1 then return 0 end
+ local PartA = self:GetOrFindCachedPart(uid1)
+ local PartB
+ if not uid2 then
+ PartB = self:GetParent()
+ else
+ PartB = self:GetOrFindCachedPart(uid2)
+ end
+ if not IsValid(PartB) then
+ if uid2 then
+ --second argument exists and failed to find anything, ERROR
+ self.invalid_parts_in_expression[uid2] = "invalid argument " .. uid2 .. " in delta_x"
+ end
+ end
+
+ if not IsValid(PartA) and uid1 then --first argument exists and failed to find anything, ERROR
+ self.invalid_parts_in_expression[uid1] = "invalid argument " .. uid1 .. " in delta_x"
+ end
+
+ if not IsValid(PartA) or not IsValid(PartB) then return 0 end
+ if not PartA.Position or not PartB.Position then return 0 end
+ self.valid_parts_in_expression[PartA] = PartA
+ self.valid_parts_in_expression[PartB] = PartB
+ return PartB:GetWorldPosition().x - PartA:GetWorldPosition().x
+end
+
+PART.Inputs.delta_y = function(self, uid1, uid2)
+ if not uid1 then return 0 end
+ local PartA = self:GetOrFindCachedPart(uid1)
+ local PartB
+ if not uid2 then
+ PartB = self:GetParent()
+ else
+ PartB = self:GetOrFindCachedPart(uid2)
+ end
+ if not IsValid(PartB) then
+ if uid2 then
+ --second argument exists and failed to find anything, ERROR
+ self.invalid_parts_in_expression[uid2] = "invalid argument " .. uid2 .. " in delta_y"
+ end
+ end
+
+ if not IsValid(PartA) and uid1 then --first argument exists and failed to find anything, ERROR
+ self.invalid_parts_in_expression[uid1] = "invalid argument " .. uid1 .. " in delta_y"
+ end
+
+ if not IsValid(PartA) or not IsValid(PartB) then return 0 end
+ if not PartA.Position or not PartB.Position then return 0 end
+ self.valid_parts_in_expression[PartA] = PartA
+ self.valid_parts_in_expression[PartB] = PartB
+ return PartB:GetWorldPosition().y - PartA:GetWorldPosition().y
+end
+
+PART.Inputs.delta_z = function(self, uid1, uid2)
+ if not uid1 then return 0 end
+ local PartA = self:GetOrFindCachedPart(uid1)
+ local PartB
+ if not uid2 then
+ PartB = self:GetParent()
+ else
+ PartB = self:GetOrFindCachedPart(uid2)
+ end
+ if not IsValid(PartB) then
+ if uid2 then
+ --second argument exists and failed to find anything, ERROR
+ self.invalid_parts_in_expression[uid2] = "invalid argument " .. uid2 .. " in delta_z"
+ end
+ end
+
+ if not IsValid(PartA) and uid1 then --first argument exists and failed to find anything, ERROR
+ self.invalid_parts_in_expression[uid1] = "invalid argument " .. uid1 .. " in delta_z"
+ end
+
+ if not IsValid(PartA) or not IsValid(PartB) then return 0 end
+ if not PartA.Position or not PartB.Position then return 0 end
+ self.valid_parts_in_expression[PartA] = PartA
+ self.valid_parts_in_expression[PartB] = PartB
+ return PartB:GetWorldPosition().z - PartA:GetWorldPosition().z
+end
+
PART.Inputs.event_alternative = function(self, uid1, num1, num2)
if not uid1 then return 0 end
+ num1 = num1 or 0
+ num2 = num2 or 1
+ local PartA = self:GetOrFindCachedPart(uid1)
- local PartA = pac.GetPartFromUniqueID(pac.Hash(pac.LocalPlayer), uid1)
- if not PartA:IsValid() then PartA = pac.FindPartByName(pac.Hash(pac.LocalPlayer), uid1, self) end
+ if not IsValid(PartA) then
+ if uid1 then --first argument exists and failed to find anything, ERROR
+ self.invalid_parts_in_expression[uid1] = "invalid argument: " .. uid1 .. " in event_alternative"
+ end
+ return 0
+ end
if PartA.ClassName == "event" then
+ self.valid_parts_in_expression[PartA] = PartA
if PartA.event_triggered then return num1 or 0
else return num2 or 0 end
- else return -1 end
+ else
+ self.invalid_parts_in_expression[uid1] = "found part, but invalid class : " .. uid1 .. " : " .. tostring(PartA) .. " in event_alternative"
+ return -1
+ end
return 0
end
+PART.Inputs.if_else_event = PART.Inputs.event_alternative
+PART.Inputs.if_event = PART.Inputs.event_alternative
+
+--normalized sine
+PART.Inputs.nsin = function(self, radians) return 0.5 + 0.5*math.sin(radians) end
+--normalized sin starting at 0 with timeex
+PART.Inputs.nsin2 = function(self, radians) return 0.5 + 0.5*math.sin(-math.pi/2 + radians) end
+--normalized cos
+PART.Inputs.ncos = function(self, radians) return 0.5 + 0.5*math.cos(radians) end
+--normalized cos starting at 0 with timeex
+PART.Inputs.ncos2 = function(self, radians) return 0.5 + 0.5*math.cos(-math.pi + radians) end
+
+--easy clamp fades
+--speed and two zero-crossing points (when it begins moving, when it goes back to 0)
+PART.Inputs.ezfade = function(self, speed, starttime, endtime)
+ speed = speed or 1
+ starttime = starttime or 0
+ self.time = self.time or pac.RealTime
+ local timeex = pac.RealTime - self.time
+ local start_offset_constant = -starttime * speed
+ local result = 0
+
+ if speed < 0 then --if negative, we can use that as a simple fadeout notation
+ speed = -speed
+ endtime = endtime or starttime
+ if endtime == 0 then endtime = 1/speed end
+ local end_offset_constant = endtime * speed
+ result = math.Clamp(end_offset_constant - timeex * speed, 0, 1)
+ elseif endtime == nil then --only a fadein
+ result = math.Clamp(start_offset_constant + timeex * speed, 0, 1)
+ else --fadein fadeout
+ local end_offset_constant = endtime * speed
+ result = math.Clamp(start_offset_constant + timeex * speed, 0, 1) * math.Clamp(end_offset_constant - timeex * speed, 0, 1)
+ end
+ return result
+end
+
+--four crossing points
+PART.Inputs.ezfade_4pt = function(self, in_starttime, in_endtime, out_starttime, out_endtime)
+ if not in_starttime or not in_endtime then
+ if not self.errors_override then self:SetError("ezfade_4pt needs at least two arguments! (in_starttime, in_endtime, out_starttime, out_endtime)") end
+ self.error = true return 0
+ end -- needs at least two args. we could assume 0 starting point and first arg is fadein end, but it'll mess up the order and confuse people
+
+ local fadein_result = 0
+ local fadeout_result = 1
+ local in_speed = 1
+ local out_speed = 1
+
+ self.time = self.time or pac.RealTime
+ local timeex = pac.RealTime - self.time
+
+ if in_starttime == in_endtime then
+ if timeex < in_starttime then
+ fadein_result = 0
+ else
+ fadein_result = 1
+ end
+ else
+ in_speed = 1 / (in_endtime - in_starttime)
+ local start_offset_constant = -in_starttime * in_speed
+ fadein_result = math.Clamp(start_offset_constant + timeex * in_speed, 0, 1)
+ end
+ if not out_starttime then --missing data, assume no fadeout
+ fadeout_result = 1
+ elseif not out_endtime then --missing data, assume no fadeout
+ fadeout_result = 1
+ else
+ if out_starttime ~= out_endtime then
+ out_speed = 1 / (out_endtime - out_starttime)
+ local end_offset_constant = out_endtime * out_speed
+ fadeout_result = math.Clamp(end_offset_constant - timeex * out_speed, 0, 1)
+ else
+ if timeex > out_starttime then
+ fadeout_result = 0
+ else
+ fadeout_result = 1
+ end
+ end
+
+ end
+ return fadein_result * fadeout_result
+end
+
+for i=1,5,1 do
+ PART.Inputs["var" .. i] = function(self, uid1)
+ if not uid1 then
+ return self["feedback_extra" .. i]
+ elseif self.last_extra_feedbacks[i][uid1] then --a thing to skip part searching when we found the part
+ return self.last_extra_feedbacks[i][uid1]["feedback_extra" .. i] or 0
+ else
+ local PartA = self:GetOrFindCachedPart(uid1)
+ if IsValid(PartA) and PartA.ClassName == "proxy" then
+ self.last_extra_feedbacks[i][uid1] = PartA
+ end
+ end
+ return 0
+ end
+ PART.Inputs["extra" .. i] = PART.Inputs["var" .. i] --alias
+end
PART.Inputs.number_operator_alternative = function(self, comp1, op, comp2, num1, num2)
- if not (comp1 and op and comp2 and num1 and num2) then return -1 end
- if not (isnumber(comp1) and isnumber(comp2) and isnumber(num1) and isnumber(num2)) then return -1 end
+ if not (comp1 and op and comp2) then return -1 end
+ if not (isnumber(comp1) and isnumber(comp2) and (isnumber(num1) or isvector(num1)) and (isnumber(num2) or isvector(num2))) then return -1 end
local b = true
if op == "=" or op == "==" or op == "equal" then
b = comp1 == comp2
@@ -381,10 +827,61 @@ PART.Inputs.number_operator_alternative = function(self, comp1, op, comp2, num1,
b = comp1 < comp2
elseif op == "<=" or op == "below or equal" or op == "less or equal" or op == "less than or equal" then
b = comp1 <= comp2
- elseif op == "~=" or op == "~=" or op == "not equal" then
+ elseif op == "~=" or op == "!=" or op == "not equal" then
b = comp1 ~= comp2
end
- if b then return num1 or 0 else return num2 or 0 end
+ if b then return num1 or 1 else return num2 or 0 end
+end
+PART.Inputs.if_else = PART.Inputs.number_operator_alternative
+
+PART.Inputs.hexadecimal_level_sequence = function(self, freq, str)
+ if not str then return 0 end
+ local index = 1 + math.ceil(#str * freq * pac.RealTime) % #str
+ return (tonumber(string.sub(str,index,index),16) or 0) / 15
+end
+
+local letter_numbers = {
+ a = 0,
+ b = 1,
+ c = 2,
+ d = 3,
+ e = 4,
+ f = 5,
+ g = 6,
+ h = 7,
+ i = 8,
+ j = 9,
+ k = 10,
+ l = 11,
+ m = 12,
+ n = 13,
+ o = 14,
+ p = 15,
+ q = 16,
+ r = 17,
+ s = 18,
+ t = 19,
+ u = 20,
+ v = 21,
+ w = 22,
+ x = 23,
+ y = 24,
+ z = 25
+}
+
+PART.Inputs.letters_level_sequence = function(self, freq, str)
+ if not str then return 0 end
+ local index = 1 + math.ceil(#str * freq * pac.RealTime) % #str
+ local lookup_result = letter_numbers[string.sub(str,index,index)] or 0
+ return lookup_result / 25
+end
+
+PART.Inputs.numberlist_level_sequence = function(self, freq, ...)
+ local args = { ... }
+ if not args[1] then return 0 end
+ local index = 1 + math.ceil(#args * freq * pac.RealTime) % #args
+
+ return args[index] or 0
end
do
@@ -616,7 +1113,7 @@ do -- scale
end
PART.Inputs.parent_scale_x = function(self) return get_scale(self, "x") end
PART.Inputs.parent_scale_y = function(self) return get_scale(self, "y") end
- PART.Inputs.parent_scale_z = function(self) return get_scale(self, "z") end
+ PART.Inputs.parent_scale_z = function(self) return get_scale(self, "z") end
end
PART.Inputs.pose_parameter = function(self, name)
@@ -637,6 +1134,28 @@ PART.Inputs.pose_parameter_true = function(self, name)
return 0
end
+PART.Inputs.bodygroup = function(self, name, uid)
+ local owner
+ if not uid then
+ if self:GetParent().Bodygroup then
+ owner = self:GetParent():GetOwner()
+ else
+ owner = get_owner(self)
+ end
+ else
+ owner = self:GetOrFindCachedPart(uid):GetOwner()
+ end
+ local bgs = owner:GetBodyGroups()
+ if bgs then
+ for i,tbl in ipairs(bgs) do
+ if tbl.name == name then return owner:GetBodygroup(tbl.id) end
+ end
+ end
+ return 0
+end
+
+PART.Inputs.model_bodygroup = PART.Inputs.bodygroup
+
PART.Inputs.command = function(self, name)
local ply = self:GetPlayerOwner()
if ply.pac_proxy_events then
@@ -655,17 +1174,25 @@ PART.Inputs.command = function(self, name)
return 0, 0, 0
end
+PART.Inputs.sequenced_event_number = function(self, name)
+ local ply = self:GetPlayerOwner()
+ if ply.pac_command_event_sequencebases then
+ if ply.pac_command_event_sequencebases[name] then
+ return ply.pac_command_event_sequencebases[name].current
+ end
+ end
+ return 0
+end
+
PART.Inputs.voice_volume = function(self)
local ply = self:GetPlayerOwner()
if not IsValid(ply) then return 0 end
return ply:VoiceVolume()
end
-
PART.Inputs.voice_volume_scale = function(self)
local ply = self:GetPlayerOwner()
return ply:GetVoiceVolumeScale()
end
-
do -- light amount
local ColorToHSV = ColorToHSV
local render = render
@@ -737,7 +1264,6 @@ do -- health and armor
return owner:Health() / owner:GetMaxHealth()
end
-
PART.Inputs.owner_armor = function(self)
local owner = self:GetPlayerOwner()
if not owner:IsValid() then return 0 end
@@ -758,7 +1284,7 @@ do -- health and armor
end
end
-do -- weapon and player color
+do -- weapon, player and owner entity/part color
local Color = Color
local function get_color(self, get, field)
local color = field and get(self)[field] or get(self)
@@ -800,6 +1326,41 @@ do -- weapon and player color
PART.Inputs.weapon_color_g = function(self) return get_color(self, get_weapon_color, "g") end
PART.Inputs.weapon_color_b = function(self) return get_color(self, get_weapon_color, "b") end
end
+
+ do
+ local function reformat_color(col, proper_in, proper_out)
+ local multiplier = 1
+ if not proper_in then multiplier = multiplier / 255 end
+ if not proper_out then multiplier = multiplier * 255 end
+ col.r = math.Clamp(col.r * multiplier,0,255)
+ col.g = math.Clamp(col.g * multiplier,0,255)
+ col.b = math.Clamp(col.b * multiplier,0,255)
+ col.a = math.Clamp(col.a * multiplier,0,255)
+ return col
+ end
+ local function get_entity_color(self, field)
+ local owner = get_owner(self)
+ local part = self:GetTarget()
+ if not self.RootOwner then --we can get the color from a pac part
+ owner = self:GetParent()
+ if not owner.GetColor then --or from the root owner entity...
+ owner = get_owner(self)
+ end
+ end
+ local color = owner:GetColor()
+ if isvector(color) then --pac parts color are vector. reformat to a color first
+ color = Color(color[1], color[2], color[3], owner.GetAlpha and owner:GetAlpha() or 255)
+ end
+ reformat_color(color, owner.ProperColorRange, part.ProperColorRange) --cram or un-cram the 255 range into 1 or vice versa
+ if field then return color[field] else return color["r"], color["g"], color["b"] end
+ end
+
+ PART.Inputs.ent_color = function(self) return get_entity_color(self) end
+ PART.Inputs.ent_color_r = function(self) return get_entity_color(self, "r") end
+ PART.Inputs.ent_color_g = function(self) return get_entity_color(self, "g") end
+ PART.Inputs.ent_color_b = function(self) return get_entity_color(self, "b") end
+ PART.Inputs.ent_color_a = function(self) return get_entity_color(self, "a") end
+ end
end
do -- ammo
@@ -815,7 +1376,6 @@ do -- ammo
return owner.GetActiveWeapon and owner:GetActiveWeapon() or owner, owner
end
-
PART.Inputs.owner_total_ammo = function(self, id)
local owner = self:GetPlayerOwner()
id = id and id:lower()
@@ -824,7 +1384,6 @@ do -- ammo
return (owner.GetAmmoCount and id) and owner:GetAmmoCount(id) or 0
end
-
PART.Inputs.weapon_primary_ammo = function(self)
local wep = get_weapon(self)
@@ -880,17 +1439,14 @@ do
if not self.feedback then return 0 end
return self.feedback[1] or 0
end
-
PART.Inputs.feedback_x = function(self)
if not self.feedback then return 0 end
return self.feedback[1] or 0
end
-
PART.Inputs.feedback_y = function(self)
if not self.feedback then return 0 end
return self.feedback[2] or 0
end
-
PART.Inputs.feedback_z = function(self)
if not self.feedback then return 0 end
return self.feedback[3] or 0
@@ -929,6 +1485,81 @@ PART.Inputs.flat_dot_right = function(self)
return 0
end
+PART.Inputs.server_maxplayers = function(self)
+ return game.MaxPlayers()
+end
+PART.Inputs.server_playercount = function(self) return #player.GetAll() end
+PART.Inputs.server_population = PART.Inputs.server_playercount
+PART.Inputs.server_botcount = function(self) return #player.GetBots() end
+PART.Inputs.server_humancount = function(self) return #player.GetHumans() end
+
+PART.Inputs.pac_healthbars_total = function(self)
+ local ent = self:GetPlayerOwner()
+ if ent.pac_healthbars then
+ return ent.pac_healthbars_total or 0
+ end
+ return 0
+end
+
+PART.Inputs.healthmod_bar_total = PART.Inputs.pac_healthbars_total
+
+PART.Inputs.pac_healthbars_layertotal = function(self, layer)
+ local ent = self:GetPlayerOwner()
+ if ent.pac_healthbars and ent.pac_healthbars_layertotals then
+ return ent.pac_healthbars_layertotals[layer] or 0
+ end
+ return 0
+end
+
+PART.Inputs.healthmod_bar_layertotal = PART.Inputs.pac_healthbars_layertotal
+
+PART.Inputs.pac_healthbar_uidvalue = function(self, uid)
+ local ent = self:GetPlayerOwner()
+ local part = self:GetOrFindCachedPart(uid)
+
+ if not IsValid(part) then
+ self.invalid_parts_in_expression[uid] = "invalid uid : " .. uid .. " in pac_healthbar_uidvalue"
+ elseif part.ClassName ~= "health_modifier" then
+ self.invalid_parts_in_expression[uid] = "invalid class : " .. uid .. " in pac_healthbar_uidvalue"
+ end
+ if ent.pac_healthbars and ent.pac_healthbars_uidtotals then
+ if ent.pac_healthbars_uidtotals[part.UniqueID] then
+
+ if part:IsValid() then
+ self.valid_parts_in_expression[part] = part
+ end
+ end
+ return ent.pac_healthbars_uidtotals[part.UniqueID] or 0
+ end
+ return 0
+end
+
+PART.Inputs.healthmod_bar_uidvalue = PART.Inputs.pac_healthbar_uidvalue
+
+PART.Inputs.pac_healthbar_remaining_bars = function(self, uid)
+ local ent = self:GetPlayerOwner()
+ local part = self:GetOrFindCachedPart(uid)
+ if not IsValid(part) then
+ self.invalid_parts_in_expression[uid] = "invalid uid or name : " .. uid .. " in pac_healthbar_remaining_bars"
+ elseif part.ClassName ~= "health_modifier" then
+ self.invalid_parts_in_expression[uid] = "invalid class : " .. uid .. " in pac_healthbar_remaining_bars"
+ end
+ if ent.pac_healthbars and ent.pac_healthbars_uidtotals then
+ if ent.pac_healthbars_uidtotals[uid] then
+
+ if part:IsValid() then
+ self.valid_parts_in_expression[part] = part
+ end
+ end
+ return part.healthbar_index or 0
+ end
+ return 0
+end
+
+PART.Inputs.healthmod_bar_remaining_bars = PART.Inputs.pac_healthbar_remaining_bars
+
+
+local proxy_verbosity = CreateConVar("pac_proxy_verbosity", 1, FCVAR_ARCHIVE, "whether to print info when running pac_proxy")
net.Receive("pac_proxy", function()
local ply = net.ReadEntity()
local str = net.ReadString()
@@ -940,6 +1571,9 @@ net.Receive("pac_proxy", function()
if ply:IsValid() then
ply.pac_proxy_events = ply.pac_proxy_events or {}
ply.pac_proxy_events[str] = {name = str, x = x, y = y, z = z}
+ if proxy_verbosity:GetBool() and pac.LocalPlayer == ply then
+ pac.Message("pac_proxy -> command(\""..str.."\") is " .. x .. "," .. y .. "," .. z)
+ end
end
end)
@@ -960,9 +1594,35 @@ local allowed = {
boolean = true,
}
-function PART:SetExpression(str)
- self.Expression = str
- self.ExpressionFunc = nil
+function PART:SetExpression(str, slot)
+ str = string.Trim(str,"\n")
+ if self == pace.current_part and (pace.ActiveSpecialPanel and pace.ActiveSpecialPanel.luapad) and str ~= "" then
+ --update luapad text if we update the expression from the properties
+ if slot == pace.ActiveSpecialPanel.luapad.keynumber then --this check prevents cross-contamination
+ pace.ActiveSpecialPanel.luapad:SetText(str)
+ end
+ end
+
+ if not slot then --that's the default expression
+ self.Expression = str
+ self.ExpressionFunc = nil
+ self.valid_parts_in_expression = {}
+ self.invalid_parts_in_expression = {}
+ elseif slot == 0 then --that's the expression on hide
+ self.ExpressionOnHide = str
+ self.ExpressionOnHideFunc = nil
+ elseif slot <= 5 then --that's one of the custom variables
+ self["CustomVariable" .. slot] = str
+ self["Extra" .. slot .. "Func"] = nil
+ self.has_extras = true
+ end
+ self.last_extra_feedbacks = {
+ [1] = {},
+ [2] = {},
+ [3] = {},
+ [4] = {},
+ [5] = {},
+ }
if str and str ~= "" then
local lib = {}
@@ -973,17 +1633,69 @@ function PART:SetExpression(str)
local ok, res = pac.CompileExpression(str, lib)
if ok then
- self.ExpressionFunc = res
- self.ExpressionError = nil
- self:SetError()
+ if not slot then --that's the default expression
+ self.ExpressionFunc = res
+ self.ExpressionError = nil
+ self:SetError()
+ elseif slot == 0 then --that's the expression on hide
+ self.ExpressionOnHideFunc = res
+ self.ExpressionOnHideError = nil
+ self:SetError()
+ elseif slot <= 5 then --that's one of the extra custom variables
+ self["Extra" .. slot .. "Func"] = res
+ self["Extra" .. slot .. "Error"] = nil
+ self:SetError()
+ end
+
else
- self.ExpressionFunc = true
- self.ExpressionError = res
- self:SetError(res)
+ if not slot then --that's the default expression
+ self.ExpressionFunc = true
+ self.ExpressionError = res
+ self:SetError(res)
+ elseif slot == 0 then --that's the expression on hide
+ self.ExpressionOnHideFunc = true
+ self.ExpressionOnHideError = res
+ self:SetError(res)
+ elseif slot <= 5 then --that's one of the extra custom variables
+ self["Extra" .. slot .. "Func"] = true
+ self["Extra" .. slot .. "Error"] = res
+ self:SetError(res)
+ end
+
end
end
end
+function PART:SetExpressionOnHide(str)
+ self.ExpressionOnHide = str
+ self:SetExpression(str, 0)
+end
+
+function PART:SetExtra1(str)
+ self.Extra1 = str
+ self:SetExpression(str, 1)
+end
+
+function PART:SetExtra2(str)
+ self.Extra2 = str
+ self:SetExpression(str, 2)
+end
+
+function PART:SetExtra3(str)
+ self.Extra3 = str
+ self:SetExpression(str, 3)
+end
+
+function PART:SetExtra4(str)
+ self.Extra4 = str
+ self:SetExpression(str, 4)
+end
+
+function PART:SetExtra5(str)
+ self.Extra5 = str
+ self:SetExpression(str, 5)
+end
+
function PART:OnHide()
self.time = nil
self.rand = nil
@@ -1009,6 +1721,10 @@ function PART:OnHide()
part.proxy_hide = nil
end
end
+
+ if self.ExpressionOnHide ~= "" and self.ExpressionOnHideFunc then
+ self:OnThink(true)
+ end
end
function PART:OnShow()
@@ -1018,16 +1734,18 @@ function PART:OnShow()
self.vec_additive = Vector()
end
+local extra_dynamic = CreateClientConVar("pac_special_property_update_dynamically", "1", true, false, "Whether proxies should refresh the properties, and some booleans may show more information.")
local function set(self, part, x, y, z, children)
local val = part:GetProperty(self.VariableName)
+ local original_x
local T = type(val)
+ local vector_type = false
if allowed[T] then
if T == "boolean" then
x = x or val == true and 1 or 0
local b = tonumber(x) > 0
-
-- special case for hide to make it behave like events
if self.VariableName == "Hide" then
@@ -1052,13 +1770,28 @@ local function set(self, part, x, y, z, children)
part:SetProperty(self.VariableName, b)
end
elseif T == "number" then
+ original_x = x
x = x or val
part:SetProperty(self.VariableName, tonumber(x) or 0)
+ self.using_x = true
else
+ vector_type = true
if self.Axis ~= "" and val[self.Axis] then
val = val * 1
- val[self.Axis] = x
+ val[self.Axis] = x or 0
+ if T == "Angle" then
+ self.using_x = self.Axis == "p" or self.Axis == "x" or self.Axis == "pitch"
+ self.using_y = self.Axis == "y" or self.Axis == "y" or self.Axis == "yaw"
+ self.using_z = self.Axis == "r" or self.Axis == "z" or self.Axis == "roll"
+ elseif T == "Vector" then
+ self.using_x = self.Axis == "x"
+ self.using_y = self.Axis == "y"
+ self.using_z = self.Axis == "z"
+ end
else
+ self.using_x = x ~= nil
+ self.using_y = y ~= nil
+ self.using_z = z ~= nil
if T == "Angle" then
val = val * 1
val.p = x or val.p
@@ -1081,6 +1814,69 @@ local function set(self, part, x, y, z, children)
set(self, part, x, y, z, true)
end
end
+
+ --update the property if this is the current part
+ if not extra_dynamic:GetBool() then return end
+ if pace:IsActive() then
+ if self:GetPlayerOwner() ~= pac.LocalPlayer then return end
+ if part ~= pace.current_part then return end
+ local property_pnl = part["pac_property_panel_"..self.VariableName]
+ if IsValid(property_pnl) then
+ local container = property_pnl:GetParent()
+ local math_description = "expression:\n"..self.Expression
+ if self.Expression == "" then math_description = "using " .. self.Function .. " and " .. self.Input end
+ if vector_type then
+ property_pnl.user_proxies = property_pnl.right.user_proxies or {}
+ property_pnl.user_proxies [self] = self
+ if self.using_x then
+ property_pnl.used_by_proxy = true
+ container = property_pnl.left
+ property_pnl.left.used_by_proxy = true
+ property_pnl.left.user_proxies = property_pnl.left.user_proxies or {}
+ property_pnl.left.user_proxies [self] = self
+ local num = x or 0
+ property_pnl.left:SetValue(math.Round(tonumber(num),4))
+ container:SetTooltip("LOCKED: Used by proxy:\n"..self:GetName().."\n\n" .. math_description)
+ end
+ if self.using_y then
+ property_pnl.used_by_proxy = true
+ container = property_pnl.middle
+ property_pnl.middle.used_by_proxy = true
+ property_pnl.middle.user_proxies = property_pnl.middle.user_proxies or {}
+ property_pnl.middle.user_proxies [self] = self
+ local num = y or x or 0
+ property_pnl.middle:SetValue(math.Round(tonumber(num),4))
+ container:SetTooltip("LOCKED: Used by proxy:\n"..self:GetName().."\n\n" .. math_description)
+ end
+ if self.using_z then
+ property_pnl.used_by_proxy = true
+ container = property_pnl.right
+ property_pnl.right.used_by_proxy = true
+ property_pnl.right.user_proxies = property_pnl.right.user_proxies or {}
+ property_pnl.right.user_proxies [self] = self
+ local num = z or x or 0
+ property_pnl.right:SetValue(math.Round(tonumber(num),4))
+ container:SetTooltip("LOCKED: Used by proxy:\n"..self:GetName().."\n\n" .. math_description)
+ end
+ elseif T == "boolean" then
+ if x ~= nil then
+ property_pnl.used_by_proxy = true
+ property_pnl.user_proxies = property_pnl.user_proxies or {}
+ property_pnl.user_proxies [self] = self
+ property_pnl:SetValue(tonumber(x) > 0)
+ container:SetTooltip("LOCKED: Used by proxy:\n"..self:GetName().."\n\n" .. math_description)
+ end
+ elseif original_x ~= nil then
+ property_pnl.used_by_proxy = true
+ property_pnl.user_proxies = property_pnl.user_proxies or {}
+ property_pnl.user_proxies [self] = self
+ property_pnl:SetValue(math.Round(tonumber(x) or 0,4))
+ container:SetTooltip("LOCKED: Used by proxy:\n"..self:GetName().."\n\n" .. math_description)
+ end
+
+
+ end
+ end
end
function PART:RunExpression(ExpressionFunc)
@@ -1090,14 +1886,88 @@ function PART:RunExpression(ExpressionFunc)
return pcall(ExpressionFunc)
end
-function PART:OnThink()
+local function get_preview_draw_string(self)
+ local str = self.debug_var or ""
+ local name = self.Name
+ if self.Name == "" then
+ name = self:GetTarget().Name
+ if name == "" then
+ name = self:GetTarget():GetNiceName()
+ end
+ end
+ str = name .. "." .. self.VariableName .. " = " .. str
+ return str
+end
+
+local function remove_line_from_previews(self)
+ text_preview_ent_rows = {}
+ if text_preview_ent_rows[self:GetOwner()] then
+ text_preview_ent_rows[self:GetOwner()][self] = {}
+ end
+ pace.flush_proxy_previews = true
+ timer.Simple(0.1, function() pace.flush_proxy_previews = false end)
+end
+
+
+local text_preview_ent_rows = {}
+
+local function draw_proxy_text(self, str)
+ if pace.flush_proxy_previews then text_preview_ent_rows = {} return end
+ local ent = self:GetOwner()
+ local pos = ent:GetPos()
+ local tbl = pos:ToScreen()
+ text_preview_ent_rows[ent] = text_preview_ent_rows[ent] or {}
+ if not text_preview_ent_rows[ent][self] then
+ text_preview_ent_rows[ent][self] = table.Count(text_preview_ent_rows[ent])
+ end
+ tbl.x = tbl.x + 20
+ tbl.y = tbl.y + 10 * text_preview_ent_rows[ent][self]
+ draw.DrawText(get_preview_draw_string(self), "ChatFont", tbl.x, tbl.y)
+end
+
+function PART:OnWorn()
+ remove_line_from_previews(self) --just for a quick refresh
+end
+
+function PART:OnRemove()
+ remove_line_from_previews(self)
+ pac.RemoveHook("HUDPaint", "proxy" .. self.UniqueID)
+end
+
+function PART:OnThink(to_hide)
local part = self:GetTarget()
if not part:IsValid() then return end
- if part.ClassName == 'woohoo' then return end
+ if part.ClassName == 'woohoo' then --why a part hardcode exclusion??
+ --ok fine I guess it's because it's super expensive, but at least we can be selective about it, the other parameters are safe
+ if self.VariableName == "Resolution" or self.VariableName == "BlurFiltering" and self.touched then
+ return
+ end
+ end
+
+ --foolproofing: scream at the user if they didn't set a variable name and there's no extra expressions ready to be used
+ if self == pace.current_part then self.touched = true end
+ if self ~= pace.current_part and self.VariableName == "" and self.touched and self.Extra1 == "" and self.Extra2 == "" and self.Extra3 == "" and self.Extra4 == "" and self.Extra5 == "" then
+ self:AttachEditorPopup("You forgot to set a variable name! The proxy won't work until it knows where to send the math!", true)
+ pace.FlashNotification("An edited proxy still has no variable name! The proxy won't work until it knows where to send the math!")
+ self:SetWarning("You forgot to set a variable name! The proxy won't work until it knows where to send the math!")
+ self.touched = false
+ elseif self.VariableName ~= "" and not self.error and not self.errors_override then self:SetWarning() end
self:CalcVelocity()
+ if self.has_extras then --pre-calculate the extra expressions if needed
+ for i=1,5,1 do
+ if self["Extra" .. i] ~= "" then
+ local ok, x,y,z = self:RunExpression(self["Extra" .. i .. "Func"])
+ if ok then
+ self["feedback_extra" .. i] = x
+ end
+ end
+ end
+ end
+
local ExpressionFunc = self.ExpressionFunc
+ if to_hide then ExpressionFunc = self.ExpressionOnHideFunc end
if not ExpressionFunc then
self:SetExpression(self.Expression)
@@ -1108,13 +1978,21 @@ function PART:OnThink()
local ok, x,y,z = self:RunExpression(ExpressionFunc)
- if not ok then
+ if not ok then self.error = true
if self:GetPlayerOwner() == pac.LocalPlayer and self.Expression ~= self.LastBadExpression then
- chat.AddText(Color(255,180,180),"============\n[ERR] PAC Proxy error on "..tostring(self)..":\n"..x.."\n============\n")
+ --don't spam the chat every time we type a single character in the luapad
+ if not (pace.ActiveSpecialPanel and pace.ActiveSpecialPanel.luapad) then
+ chat.AddText(Color(255,180,180),"============\n[ERR] PAC Proxy error on "..tostring(self)..":\n"..x.."\n============\n")
+ end
self.LastBadExpression = self.Expression
+ self.Error = x --will be used by the luapad for its title
end
+
+ if not self.errors_override then self:SetError(self.Error) end
return
end
+ self.Error = nil
+ if not self.errors_override then self:SetError() end
if x and not isnumber(x) then x = 0 end
if y and not isnumber(y) then y = 0 end
@@ -1144,11 +2022,25 @@ function PART:OnThink()
self.feedback[3] = z
if self.AffectChildren then
- for _, part in ipairs(self:GetChildren()) do
- set(self, part, x, y, z, true)
+ if self.MultiTargetPart then
+ for _,part2 in ipairs(self.MultiTargetPart) do
+ if not part2.GetProperty then continue end
+ set(self, part2, x, y, z, true)
+ end
+ else
+ for _, part in ipairs(self:GetChildren()) do
+ set(self, part, x, y, z, true)
+ end
end
else
- set(self, part, x, y, z)
+ if self.MultiTargetPart then
+ for _,part2 in ipairs(self.MultiTargetPart) do
+ if not part2.GetProperty then continue end
+ set(self, part2, x, y, z)
+ end
+ else
+ set(self, part, x, y, z)
+ end
end
if pace and pace.IsActive() then
@@ -1163,7 +2055,7 @@ function PART:OnThink()
local T = type(val)
if T == "boolean" then
- str = tonumber(x) > 0 and "true" or "false"
+ str = (tonumber(x) or 0) > 0 and "true" or "false"
elseif T == "Vector" then
str = "Vector(" .. str .. ")"
elseif T == "Angle" then
@@ -1202,11 +2094,26 @@ function PART:OnThink()
end
if self.AffectChildren then
- for _, part in ipairs(self:GetChildren()) do
- set(self, part, num, nil, nil, true)
+ if self.MultiTargetPart then
+ for _,part2 in ipairs(self.MultiTargetPart) do
+ if not part2.GetProperty then continue end
+ set(self, part2, num, nil, nil, true)
+ end
+ else
+ for _, part in ipairs(self:GetChildren()) do
+ set(self, part, num, nil, nil, true)
+ end
end
else
- set(self, part, num)
+ if self.MultiTargetPart then
+ for _,part2 in ipairs(self.MultiTargetPart) do
+ if not part2.GetProperty then continue end
+ set(self, part2, num)
+ end
+ else
+ set(self, part, num)
+ end
+
end
if pace and pace.IsActive() then
@@ -1219,6 +2126,68 @@ function PART:OnThink()
end
end
+ if table.Count(self.invalid_parts_in_expression) > 0 then
+ local error_msg = ""
+ for str, message in pairs(self.invalid_parts_in_expression) do
+ error_msg = error_msg .. " " .. message .. "\n"
+ end
+ self:SetError(error_msg) self.error = true
+ end
+ if self:GetPlayerOwner() == pac.LocalPlayer then
+ if self.PreviewOutput then
+ pac.AddHook("HUDPaint", "proxy" .. self.UniqueID, function() draw_proxy_text(self, str) end)
+ else
+ pac.RemoveHook("HUDPaint", "proxy" .. self.UniqueID)
+ end
+ end
+
end
+
+function PART:GetActiveFunctions()
+ if self.Expression == "" then return {self.Input, self.Function} end
+ local possible_funcs = {}
+ for kw,_ in pairs(PART.Inputs) do
+ local kw2 = kw .. "("
+ if string.find(self.Expression, kw2, 0, true) ~= nil then
+ table.insert(possible_funcs, kw)
+ end
+ end
+
+ return possible_funcs
+end
+
+function PART:GetTutorial(str)
+ if not str then
+ if pace and pace.TUTORIALS then
+ return pace.TUTORIALS.PartInfos[self.ClassName].popup_tutorial
+ end
+ end
+ return PART.Tutorials[str]
+end
+
+function PART:AttachEditorPopup(str, flash, tbl)
+ if str == nil then
+ local funcs = self:GetActiveFunctions()
+ if #funcs > 0 then
+ str = "active functions"
+ if self.Expression ~= "" then
+ str = self.Expression .. "\n\nactive functions"
+ end
+ for i, kw in ipairs(self:GetActiveFunctions()) do
+ str = str .. "\n\n====================================================================\n\n" .. self:GetTutorial(kw)
+ end
+ end
+ end
+ local pnl = self:SetupEditorPopup(str, flash, tbl)
+ if flash and pnl then
+ pnl:MakePopup()
+ end
+ return pnl
+end
+
+timer.Simple(10, function()
+ pace.TUTORIALS["proxy_functions"] = PART.Tutorials
+end)
+
BUILDER:Register()
diff --git a/lua/pac3/core/client/parts/shake.lua b/lua/pac3/core/client/parts/shake.lua
index 5fe8a3028..ae2124bd9 100644
--- a/lua/pac3/core/client/parts/shake.lua
+++ b/lua/pac3/core/client/parts/shake.lua
@@ -4,6 +4,10 @@ PART.ClassName = "shake"
PART.Group = 'effects'
PART.Icon = 'icon16/transmit.png'
+local draw_distance = CreateClientConVar("pac_limit_shake_draw_distance", "500", true, false)
+local max_duration = CreateClientConVar("pac_limit_shake_duration", "2", true, false)
+local max_amplitude = CreateClientConVar("pac_limit_shake_amplitude", "40", true, false)
+
BUILDER:StartStorableVars()
BUILDER:SetPropertyGroup("generic")
BUILDER:SetPropertyGroup("shake")
@@ -18,14 +22,18 @@ function PART:OnShow(from_rendering)
if not from_rendering then
local position = self:GetDrawPosition()
local eyedistance = position:Distance(pac.EyePos)
- local radius = math.Clamp(self.Radius, 0.0001, 500)
+
+ --clamp down the part's requested values with the viewer client's cvar
+ local radius = math.Clamp(self.Radius, 0.0001, math.max(draw_distance:GetInt(),0.0001))
+ local duration = math.Clamp(self.Duration, 0.0001, max_duration:GetInt())
+ local amplitude = math.Clamp(self.Amplitude, 0.0001, max_amplitude:GetInt())
if eyedistance < radius then
local amplitude = self.Amplitude
if self.Falloff then
amplitude = amplitude * (1 - (eyedistance / radius))
end
- util.ScreenShake(position, amplitude, self.Frequency, math.Clamp(self.Duration, 0.0001, 2), 0)
+ util.ScreenShake(position, amplitude, self.Frequency, duration, 0)
end
end
end
diff --git a/lua/pac3/core/client/parts/sound.lua b/lua/pac3/core/client/parts/sound.lua
index da1f90d84..c60d49fec 100644
--- a/lua/pac3/core/client/parts/sound.lua
+++ b/lua/pac3/core/client/parts/sound.lua
@@ -8,9 +8,12 @@ PART.ClassName = "sound2"
PART.Icon = 'icon16/music.png'
PART.Group = 'effects'
+PART.ImplementsDoubleClickSpecified = true
+
BUILDER:StartStorableVars()
BUILDER:SetPropertyGroup("generic")
BUILDER:GetSet("Path", "", {editor_panel = "sound"})
+ BUILDER:GetSet("AllPaths", "", {hide_in_editor = true})
BUILDER:GetSet("Volume", 1, {editor_sensitivity = 0.25})
BUILDER:GetSet("Pitch", 1, {editor_sensitivity = 0.125})
BUILDER:GetSet("Radius", 1500)
@@ -128,13 +131,29 @@ BIND("VolumeLFOTime")
BIND("Doppler")
+function PART:Silence(b)
+ if b then
+ if self.last_stream and self.last_stream.SetVolume then self.last_stream:SetVolume(0) end
+ else
+ if self.last_stream and self.last_stream.SetVolume then self.last_stream:SetVolume(self.Volume * pac.volume) end
+ end
+end
+
function PART:OnThink()
local owner = self:GetRootPart():GetOwner()
+ local pos = self:GetWorldPosition()
+ if pos:DistToSqr(pac.EyePos) > pac.sounds_draw_dist_sqr then
+ self.out_of_range = true
+ self:Silence(true)
+ else
+ if self.out_of_range then self:Silence(false) end
+ self.out_of_range = false
+ end
for url, stream in pairs(self.streams) do
if not stream:IsValid() then self.streams[url] = nil goto CONTINUE end
- if self.PlayCount == 0 then
+ if self.PlayCount == 0 and not self.stopsound then
stream:Resume()
end
@@ -166,6 +185,15 @@ function PART:OnThink()
end
function PART:SetPath(path)
+ if #path > 1024 then
+ self:AttachEditorPopup("This part has more sounds than the 1024-letter limit! Please do not touch the path field now!")
+ self:SetInfo("This part has more sounds than the 1024-letter limit! Please do not touch the path field now!")
+ if self.Name == "" then
+ self:SetName("big sound list")
+ pace.RefreshTree()
+ end
+ end
+
self.seq_index = 1
self.Path = path
@@ -180,10 +208,13 @@ function PART:SetPath(path)
if min and max then
for i = min, max do
table.insert(paths, (path:gsub("%[.-%]", i)))
+ self.AllPaths = self.AllPaths .. ";" .. path
end
else
table.insert(paths, path)
+ self.AllPaths = self.AllPaths .. ";" .. path
end
+
end
for _, stream in pairs(self.streams) do
@@ -245,13 +276,20 @@ function PART:SetPath(path)
end
end
self.paths = paths
+
end
PART.last_stream = NULL
+function PART:UpdateSoundsFromAll()
+ self:SetPath(self.AllPaths)
+end
+
function PART:PlaySound(_, additiveVolumeFraction)
--PrintTable(self.streams)
additiveVolumeFraction = additiveVolumeFraction or 0
+ local pos = self:GetWorldPosition()
+ if pos:DistToSqr(pac.EyePos) > pac.sounds_draw_dist_sqr then return end
local stream = table.Random(self.streams) or NULL
if not stream:IsValid() then return end
@@ -271,7 +309,7 @@ function PART:PlaySound(_, additiveVolumeFraction)
if self.streams[snd]:IsValid() then
stream = self.streams[snd]
- print(snd,self.seq_index)
+ --print(snd,self.seq_index)
end
self.seq_index = self.seq_index + self.SequentialStep
if self.seq_index > #self.paths then
@@ -302,7 +340,7 @@ function PART:PlaySound(_, additiveVolumeFraction)
self.last_stream = stream
end
-function PART:StopSound()
+function PART:StopSound(force_stop)
for key, stream in pairs(self.streams) do
if stream:IsValid() then
if self.PauseOnHide then
@@ -310,6 +348,7 @@ function PART:StopSound()
elseif self.StopOnHide then
stream:Stop()
end
+ if force_stop then stream:Stop() self.stopsound = true end
end
end
end
@@ -318,6 +357,17 @@ function PART:OnShow(from_rendering)
if not from_rendering then
self:PlaySound()
end
+ self.stopsound = false
+end
+
+function PART:OnDoubleClickSpecified()
+ if self.playing then
+ self:StopSound(true)
+ self.playing = false
+ else
+ self:PlaySound()
+ self.playing = true
+ end
end
function PART:OnHide()
@@ -333,4 +383,4 @@ function PART:OnRemove()
end
end
-BUILDER:Register()
+BUILDER:Register()
\ No newline at end of file
diff --git a/lua/pac3/core/client/parts/sprite.lua b/lua/pac3/core/client/parts/sprite.lua
index 71e227fe0..4aa85dd2e 100644
--- a/lua/pac3/core/client/parts/sprite.lua
+++ b/lua/pac3/core/client/parts/sprite.lua
@@ -24,8 +24,25 @@ BUILDER:StartStorableVars()
BUILDER:GetSet("Color", Vector(255, 255, 255), {editor_panel = "color"})
BUILDER:GetSet("Alpha", 1, {editor_sensitivity = 0.25, editor_clamp = {0, 1}})
BUILDER:GetSet("Translucent", true)
+
+ BUILDER:SetPropertyGroup("Showtime dynamics")
+ BUILDER:GetSet("EnableDynamics", false, {description = "If you want to make a fading effect, you can do it here instead of adding proxies."})
+ BUILDER:GetSet("SizeFadeSpeed", 1)
+ BUILDER:GetSet("SizeFadePower", 1)
+ BUILDER:GetSet("DynamicsStartSizeMultiplier", 1, {editor_friendly = "StartSizeMultiplier"})
+ BUILDER:GetSet("DynamicsEndSizeMultiplier", 1, {editor_friendly = "EndSizeMultiplier"})
+
+ BUILDER:GetSet("AlphaFadeSpeed", 1)
+ BUILDER:GetSet("AlphaFadePower", 1)
+ BUILDER:GetSet("DynamicsStartAlpha", 1, {editor_sensitivity = 0.25, editor_clamp = {0, 1}, editor_friendly = "StartAlpha"})
+ BUILDER:GetSet("DynamicsEndAlpha", 1, {editor_sensitivity = 0.25, editor_clamp = {0, 1}, editor_friendly = "EndAlpha"})
+
BUILDER:EndStorableVars()
+function PART:OnShow()
+ self.starttime = CurTime()
+end
+
function PART:GetNiceName()
if not self:GetSpritePath() then
return "error"
@@ -110,6 +127,24 @@ function PART:OnDraw()
end
local old_alpha
+
+ local lifetime = (CurTime() - self.starttime)
+
+ local fade_factor_s = math.Clamp(lifetime*self.SizeFadeSpeed,0,1)
+ local fade_factor_a = math.Clamp(lifetime*self.AlphaFadeSpeed,0,1)
+ local final_alpha_mult = self.EnableDynamics and
+ self.DynamicsStartAlpha + (self.DynamicsEndAlpha - self.DynamicsStartAlpha) * math.pow(fade_factor_a,self.AlphaFadePower)
+ or 1
+
+ local final_size_mult = self.EnableDynamics and
+ self.DynamicsStartSizeMultiplier + (self.DynamicsEndSizeMultiplier - self.DynamicsStartSizeMultiplier) * math.pow(fade_factor_s,self.SizeFadePower)
+ or 1
+
+ if self.EnableDynamics then
+ if not self.ColorC then self:SetColor(self:GetColor()) end
+ self.ColorC.a = self.Alpha * 255 * final_alpha_mult
+ end
+
if pac.drawing_motionblur_alpha then
if not self.ColorC then self:SetColor(self:GetColor()) end
old_alpha = self.ColorC.a
@@ -120,7 +155,7 @@ function PART:OnDraw()
local pos = self:GetDrawPosition()
render_SetMaterial(mat)
- render_DrawSprite(pos, self.SizeX * self.Size, self.SizeY * self.Size, self.ColorC)
+ render_DrawSprite(pos, self.SizeX * self.Size * final_size_mult, self.SizeY * self.Size * final_size_mult, self.ColorC)
if self.IgnoreZ then
cam_IgnoreZ(false)
@@ -129,6 +164,7 @@ function PART:OnDraw()
if pac.drawing_motionblur_alpha then
self.ColorC.a = old_alpha
end
+
end
end
diff --git a/lua/pac3/core/client/parts/submaterial.lua b/lua/pac3/core/client/parts/submaterial.lua
index d3b53ae58..7e4c5df0c 100644
--- a/lua/pac3/core/client/parts/submaterial.lua
+++ b/lua/pac3/core/client/parts/submaterial.lua
@@ -23,6 +23,7 @@ BUILDER:StartStorableVars()
return tbl
end,
})
+ BUILDER:GetSet("UnlitMaterialHack", false, {description = "hacky fix if the material comes from a material part with unlitgeneric shader.\nBut it will break raw url textures!"})
BUILDER:GetSet("RootOwner", false, { hide_in_editor = true })
BUILDER:EndStorableVars()
@@ -63,6 +64,7 @@ function PART:UpdateSubMaterialId(id, material)
ent.pac_submaterials = ent.pac_submaterials or {}
local mat = self.Materialm
+ if self.UnlitMaterialHack then mat = pac.Material(self.Material, self) end
if not material then
if self.Material and self.Material ~= "" and mat and not mat:IsError() then
@@ -79,6 +81,11 @@ function PART:UpdateSubMaterialId(id, material)
end
end
+function PART:SetUnlitMaterialHack(b)
+ self.UnlitMaterialHack = b
+ self:UpdateSubMaterialId()
+end
+
function PART:PostApplyFixes()
self:UpdateSubMaterialId()
end
diff --git a/lua/pac3/core/client/parts/sunbeams.lua b/lua/pac3/core/client/parts/sunbeams.lua
index 2793c1063..50cdea0ff 100644
--- a/lua/pac3/core/client/parts/sunbeams.lua
+++ b/lua/pac3/core/client/parts/sunbeams.lua
@@ -7,12 +7,29 @@ local BUILDER, PART = pac.PartTemplate("base_drawable")
PART.ClassName = "sunbeams"
PART.Group = 'effects'
PART.Icon = 'icon16/weather_sun.png'
+local draw_distance = CreateClientConVar("pac_limit_sunbeams_draw_distance", "1000", true, false)
+
BUILDER:StartStorableVars()
- BUILDER:GetSet("Darken", 0)
- BUILDER:GetSet("Multiplier", 0.25, {editor_sensitivity = 0.25})
- BUILDER:GetSet("Size", 0.1, {editor_sensitivity = 0.25})
- BUILDER:GetSet("Translucent", true)
+ BUILDER:GetSet("Darken", 0)
+ BUILDER:GetSet("Multiplier", 0.25, {editor_sensitivity = 0.25})
+ BUILDER:GetSet("Size", 0.1, {editor_sensitivity = 0.25})
+ BUILDER:GetSet("DrawDistance", 1000, {editor_onchange = function(self, val) return math.max(val,0) end})
+ BUILDER:GetSet("Translucent", true)
+
+ BUILDER:SetPropertyGroup("Showtime dynamics")
+ BUILDER:GetSet("EnableDynamics", false, {description = "If you want to make a fading effect, you can do it here instead of adding proxies.\nThe multiplier parts work multiplicatively, involving 3 terms: attack * multiplier * fade\nThe darken part works additively. It can add more darken on top of the existing darken value"})
+ BUILDER:GetSet("EndMultiplier", 1)
+ BUILDER:GetSet("StartMultiplier", 1)
+ BUILDER:GetSet("MultiplierFadePower", 1)
+ BUILDER:GetSet("MultiplierFadeSpeed", 1)
+ BUILDER:GetSet("MultiplierAttack", 0, {description = "Additional fade-in time to optionally soften the flash. This is in terms of normalized speed"})
+
+ BUILDER:GetSet("EndDarken", 0)
+ BUILDER:GetSet("StartDarken", 0)
+ BUILDER:GetSet("DarkenFadeSpeed", 1)
+ BUILDER:GetSet("DarkenFadePower", 1)
+
BUILDER:EndStorableVars()
function PART:GetNiceName()
@@ -20,6 +37,10 @@ function PART:GetNiceName()
return mult > 0 and "bright sunbeams" or mult < 0 and "dark sunbeams" or self.ClassName
end
+function PART:OnShow()
+ self.starttime = CurTime()
+end
+
function PART:OnDraw()
if not DrawSunbeams then DrawSunbeams = _G.DrawSunbeams end
@@ -27,15 +48,43 @@ function PART:OnDraw()
local pos = self:GetDrawPosition()
local spos = pos:ToScreen()
- local dist_mult = - math.Clamp(pac.EyePos:Distance(pos) / 1000, 0, 1) + 1
+ --clamp down the part's requested values with the viewer client's cvar
+ local distance = math.min(self.DrawDistance, math.max(draw_distance:GetInt(),0.1))
+
+ local dist_mult = - math.Clamp(pac.EyePos:Distance(pos) / distance, 0, 1) + 1
+
+ if self.EnableDynamics then
+ local lifetime = (CurTime() - self.starttime)
+
+ local init_mult = 1
+ if self.MultiplierAttack > 0 then init_mult = math.Clamp(lifetime*self.MultiplierAttack,0,1) end
+
+ local fade_factor_m = math.Clamp(lifetime*self.MultiplierFadeSpeed,0,1)
+ local fade_factor_d = math.Clamp(lifetime*self.DarkenFadeSpeed,0,1)
+ local final_mult_mult = self.EnableDynamics and
+ self.StartMultiplier + (self.EndMultiplier - self.StartMultiplier) * math.pow(fade_factor_m,self.MultiplierFadePower)
+ or self.Multiplier
+
+ local final_darken_add = self.EnableDynamics and
+ self.StartDarken + (self.EndDarken - self.StartDarken) * math.pow(fade_factor_d,self.DarkenFadePower)
+ or 0
- DrawSunbeams(
- self.Darken,
- dist_mult * self.Multiplier * (math.Clamp(pac.EyeAng:Forward():Dot((pos - pac.EyePos):GetNormalized()) - 0.5, 0, 1) * 2) ^ 5,
- self.Size,
- spos.x / ScrW(),
- spos.y / ScrH()
- )
+ DrawSunbeams(
+ self.Darken + final_darken_add,
+ dist_mult * init_mult * self.Multiplier * final_mult_mult * (math.Clamp(pac.EyeAng:Forward():Dot((pos - pac.EyePos):GetNormalized()) - 0.5, 0, 1) * 2) ^ 5,
+ self.Size,
+ spos.x / ScrW(),
+ spos.y / ScrH()
+ )
+ else
+ DrawSunbeams(
+ self.Darken,
+ dist_mult * self.Multiplier * (math.Clamp(pac.EyeAng:Forward():Dot((pos - pac.EyePos):GetNormalized()) - 0.5, 0, 1) * 2) ^ 5,
+ self.Size,
+ spos.x / ScrW(),
+ spos.y / ScrH()
+ )
+ end
cam.End2D()
end
diff --git a/lua/pac3/core/client/parts/text.lua b/lua/pac3/core/client/parts/text.lua
index 2eb6f4ca5..c79ce9239 100644
--- a/lua/pac3/core/client/parts/text.lua
+++ b/lua/pac3/core/client/parts/text.lua
@@ -13,6 +13,49 @@ local Color = Color
local BUILDER, PART = pac.PartTemplate("base_drawable")
+local draw_distance = CreateClientConVar("pac_limit_text_2d_draw_distance", "1000", true, false, "How far to see other players' text parts using 2D modes. They will start fading out 200 units before this distance.")
+
+
+net.Receive("pac_chat_typing_mirror_broadcast", function(len)
+ local text = net.ReadString()
+ local ply = net.ReadEntity()
+ ply.pac_mirrored_chat_text = text
+end)
+
+local TTT_fonts = {
+ "DefaultBold",
+ "TabLarge",
+ "Trebuchet22",
+ "TraitorState",
+ "TimeLeft",
+ "HealthAmmo",
+ "cool_small",
+ "cool_large",
+ "treb_small"
+}
+
+local sandbox_fonts = {
+ "GModToolName",
+ "GModToolSubtitle",
+ "GModToolHelp",
+ "GModToolScreen",
+ "ContentHeader",
+ "GModWorldtip",
+ "ContentHeader",
+}
+
+--all "de facto" usables:
+--base gmod fonts
+--sandbox OR TTT gamemode fonts
+--created fonts that passed all checks
+local usable_fonts = {}
+
+--all base usable:
+--base gmod fonts
+--sandbox OR TTT gamemode fonts
+local included_fonts = {}
+
+
local default_fonts = {
"BudgetLabel",
"CenterPrintText",
@@ -61,24 +104,60 @@ local default_fonts = {
"GModNotify",
"ScoreboardDefault",
"ScoreboardDefaultTitle",
- "GModToolName",
- "GModToolSubtitle",
- "GModToolHelp",
- "GModToolScreen",
- "ContentHeader",
- "GModWorldtip",
- "ContentHeader",
- "DefaultBold",
- "TabLarge",
- "Trebuchet22",
- "TraitorState",
- "TimeLeft",
- "HealthAmmo",
- "cool_small",
- "cool_large",
- "treb_small"
+}
+
+if engine.ActiveGamemode() == "sandbox" then
+ for i,v in ipairs(sandbox_fonts) do
+ table.insert(default_fonts,v)
+ end
+elseif engine.ActiveGamemode() == "ttt" then
+ for i,v in ipairs(TTT_fonts) do
+ table.insert(default_fonts,v)
+ end
+end
+
+for i,v in ipairs(default_fonts) do
+ usable_fonts[v] = true
+ default_fonts[v] = v --I want string key lookup...
+ included_fonts[v] = v
+end
+
+local gmod_basefonts = {
+ --key is ttf filename, value is the nice title
+ ["akbar"] = "Akbar",
+ ["coolvetica"] = "Coolvetica",
+ ["csd"] = "csd",
+ ["Roboto-Black"] = "Roboto Black",
+ ["Roboto-BlackItalic"] = "Roboto Black Italic",
+ ["Roboto-Bold"] = "Roboto Bold",
+ ["Roboto-BoldCondensed"] = "Roboto Bold Condensed",
+ ["Roboto-BoldCondensedItalic"] = "Roboto Bold Condensed Italic",
+ ["Roboto-Condensed"] = "Roboto Condensed",
+ ["Roboto-CondensedItalic"] = "Roboto Condensed Italic",
+ ["Roboto-Italic"] = "Roboto Italic",
+ ["Roboto-Light"] = "Roboto Light",
+ ["Roboto-LightItalic"] = "Roboto Light Italic",
+ ["Roboto-Medium"] = "Roboto Medium",
+ ["Roboto-MediumItalic"] = "Roboto Medium Italic",
+ ["Roboto-Regular"] = "Roboto Regular",
+ ["Roboto-Thin"] = "Roboto Thin",
+ ["Roboto-Thin"] = "Roboto Thin Italic",
+ ["Tahoma"] = "Tahoma"
}
+local buildable_basefonts = {}
+--create some fonts
+for k,v in pairs(gmod_basefonts) do
+ buildable_basefonts[v] = v
+ local newfont = v .. "_30"
+ surface.CreateFont(newfont, {
+ font = v,
+ size = 30
+ })
+ table.insert(default_fonts, newfont)
+ usable_fonts[newfont] = true
+end
+
PART.ClassName = "text"
PART.Group = "effects"
@@ -88,24 +167,34 @@ BUILDER:StartStorableVars()
:SetPropertyGroup("generic")
:PropertyOrder("Name")
:PropertyOrder("Hide")
- :GetSet("Text", "")
- :GetSet("Font", "default", {enums = default_fonts})
+ :GetSet("Text", "", {editor_panel = "generic_multiline"})
+ :GetSet("Font", "DermaDefault", {enums = default_fonts})
:GetSet("Size", 1, {editor_sensitivity = 0.25})
:GetSet("DrawMode", "DrawTextOutlined", {enums = {
["draw.SimpleTextOutlined 3D2D"] = "DrawTextOutlined",
["draw.SimpleTextOutlined 2D"] = "DrawTextOutlined2D",
- ["surface.DrawText"] = "SurfaceText"
+ ["surface.DrawText"] = "SurfaceText",
+ ["draw.DrawText"] = "DrawDrawText"
}})
:SetPropertyGroup("text layout")
:GetSet("HorizontalTextAlign", TEXT_ALIGN_CENTER, {enums = {["Left"] = "0", ["Center"] = "1", ["Right"] = "2"}})
:GetSet("VerticalTextAlign", TEXT_ALIGN_CENTER, {enums = {["Center"] = "1", ["Top"] = "3", ["Bottom"] = "4"}})
:GetSet("ConcatenateTextAndOverrideValue",false,{editor_friendly = "CombinedText"})
- :GetSet("TextPosition","Prefix", {enums = {["Prefix"] = "Prefix", ["Postfix"] = "Postfix"}},{editor_friendly = "ConcatenateMode"})
+ :GetSet("TextPosition","Prefix", {enums = {["Prefix"] = "Prefix", ["Postfix"] = "Postfix"}, description = "where the base text will be relative to the data-sourced text. this only applies when using Combined Text mode"})
+ :GetSet("SentenceNewlines", false, {description = "With the punctuation marks . ! ? make a newline. Newlines only work with DrawDrawText mode.\nThis variable is useful for the chat modes since you can't put newlines in chat.\nBut if you're not using these, you might as well use the multiline text editor on the main text's [...] button"})
+ :GetSet("Truncate", false, {description = "whether to cut the string off until a certain position. This can be used with proxies to gradually write the text.\nSkip Characters should normally be spaces and punctuation\nTruncate Words splits into words"})
+ :GetSet("TruncateWords", false, {description = "whether to cut the string off until a certain position. This can be used with proxies to gradually write the text"})
+ :GetSet("TruncateSkipCharacters", "", {description = "Characters to skip during truncation, or to split into words with the TruncateWords mode.\nNormally it could be a space, but if you want to split your text by lines (i.e. write one whole line at a time), write the escape character \"\\n\""})
+ :GetSet("TruncatePosition", 0, {editor_onchange = function(self, val) return math.floor(val) end})
+ :GetSet("VectorBrackets", "()")
+ :GetSet("VectorSeparator", ",")
+ :GetSet("UseBracketsOnNonVectors", false)
:SetPropertyGroup("data source")
:GetSet("TextOverride", "Text", {enums = {
["Proxy value (DynamicTextValue)"] = "Proxy",
+ ["Proxy vector (DynamicVectorValue)"] = "ProxyVector",
["Text"] = "Text",
["Health"] = "Health",
["Maximum Health"] = "MaxHealth",
@@ -133,9 +222,12 @@ BUILDER:StartStorableVars()
["Ground Entity Class"] = "GroundEntityClass",
["Players"] = "Players",
["Max Players"] = "MaxPlayers",
- ["Difficulty"] = "GameDifficulty"
+ ["Difficulty"] = "GameDifficulty",
+ ["Chat Being Typed"] = "ChatTyping",
+ ["Last Chat Sent"] = "ChatSent",
}})
:GetSet("DynamicTextValue", 0)
+ :GetSet("DynamicVectorValue", Vector(0,0,0))
:GetSet("RoundingPosition", 2, {editor_onchange = function(self, num)
return math.Round(num,0)
end})
@@ -158,7 +250,7 @@ BUILDER:StartStorableVars()
BUILDER:GetSet("Translucent", true)
:SetPropertyGroup("CustomFont")
:GetSet("CreateCustomFont",false, {description = "Tries to create a custom font.\nHeavily throttled as creating fonts is an expensive process.\nSupport is limited because of the fonts' supported features and the limits of Lua strings.\nFont names include those stored in your operating system. for example: Comic Sans MS, Ink Free"})
- :GetSet("CustomFont", "DermaDefault")
+ :GetSet("CustomFont", "DermaDefault", {enums = buildable_basefonts})
:GetSet("FontSize", 13)
:GetSet("FontWeight",500)
:GetSet("FontBlurSize",0)
@@ -176,6 +268,9 @@ BUILDER:EndStorableVars()
function PART:GetNiceName()
if self.TextOverride ~= "Text" then return self.TextOverride end
+ if string.find(self.Text, "\n") then
+ if self.DrawMode == "DrawDrawText" then return "multiline text" else return string.Replace(self.Text, "\n", "") end
+ end
return 'Text: "' .. self:GetText() .. '"'
end
@@ -197,6 +292,23 @@ function PART:SetAlpha(n)
self.Alpha = n
end
+function PART:SetTruncateSkipCharacters(str)
+ self.TruncateSkipCharacters = str
+ if str == "" then self.TruncateSkipCharacters_tbl = nil return end
+ self.TruncateSkipCharacters_tbl = {}
+ for i=1,#str,1 do
+ local char = str[i]
+ if char == [[\]] then
+ if str[i+1] == "n" then self.TruncateSkipCharacters_tbl["\n"] = true
+ elseif str[i+1] == "t" then self.TruncateSkipCharacters_tbl["\t"] = true
+ elseif str[i+1] == [[\]] then self.TruncateSkipCharacters_tbl["\\"] = true
+ end
+ elseif str[i-1] ~= [[\]] then
+ self.TruncateSkipCharacters_tbl[char] = true
+ end
+ end
+end
+
function PART:SetOutlineColor(v)
self.OutlineColorC = self.OutlineColorC or Color(255, 255, 255, 255)
@@ -214,42 +326,144 @@ function PART:SetOutlineAlpha(n)
self.OutlineAlpha = n
end
+local function GetReasonBadFont_At_CreationTime(str)
+ local reason
+ if #str < 20 then
+ if not included_fonts[str] then reason = str .. " is not a font that exists" end
+ if engine.ActiveGamemode() ~= "sandbox" then
+ if table.HasValue(sandbox_fonts,str) then
+ reason = str .. " is a sandbox-exclusive font not available in the gamemode " .. engine.ActiveGamemode()
+ end
+ end
+ if engine.ActiveGamemode() ~= "ttt" then
+ if table.HasValue(TTT_fonts,str) then
+ reason = str .. " is a TTT-exclusive font not available in the gamemode " .. engine.ActiveGamemode()
+ end
+ end
+ else --standard part UID length
+ if #str > 31 then
+ reason = "you cannot create fonts with the base font being longer than 31 letters"
+ end
+ end
+ if string.find(str, "http") then
+ reason = "urls are not supported"
+ end
+ return reason
+end
+
+local function GetReasonBadFont_At_UseTime(str)
+ local reason
+ if #str < 20 then
+ if not included_fonts[str] then reason = str .. " is not a font that exists" end
+ if engine.ActiveGamemode() ~= "sandbox" then
+ if table.HasValue(sandbox_fonts,str) then
+ reason = str .. " is a sandbox-exclusive font not available in the gamemode " .. engine.ActiveGamemode()
+ end
+ end
+ if engine.ActiveGamemode() ~= "ttt" then
+ if table.HasValue(TTT_fonts,str) then
+ reason = str .. " is a TTT-exclusive font not available in the gamemode " .. engine.ActiveGamemode()
+ end
+ end
+ else --standard part UID length
+ reason = str .. " is possibly a pac custom font from another text part but it's not guaranteed to be created right now\nor maybe it doesn't exist"
+ end
+ if string.find(str, "http") then
+ reason = "urls are not supported"
+ end
+ return reason
+end
+
+
+function PART:CheckFontBuildability(str)
+ if string.find(str, "http") then
+ return false, "urls are not supported"
+ end
+ if #str > 31 then return false, "base font is too long" end
+ if buildable_basefonts[str] then return true, "base font recognized from gmod" end
+ if included_fonts[str] then return true, "default font" end
+ return false, "nonexistent base font"
+end
+
+
+--before using a font, we need to check if it exists
+--font creation time should mark them
function PART:SetFont(str)
- self.UsedFont = str
- if not self.CreateCustomFont then
- if not pcall(surface_SetFont, str) then
- if #self.Font > 20 then
-
- self.lastwarn = self.lastwarn or CurTime()
- if self.lastwarn > CurTime() + 1 then
- pac.Message(Color(255,150,0),str.." Font not found! Could be custom font, trying again in 4 seconds!")
- self.lastwarn = CurTime()
+ self.Font = str
+ self:SetError()
+ self:CheckFont()
+end
+
+
+local lastfontcreationtime = 0
+function PART:CheckFont()
+ if self.CreateCustomFont then
+ if not self:CheckFontBuildability(self.CustomFont) then
+ self.UsedFont = "DermaDefault"
+ self:SetError(GetReasonBadFont_At_UseTime(self.CustomFont) .. "\nreverting to " .. self.UsedFont)
+ else
+ self:TryCreateFont()
+ end
+ else
+ if usable_fonts[self.Font] then
+ self.UsedFont = self.Font
+ else
+ self.UsedFont = "DermaDefault"
+ self:SetError(GetReasonBadFont_At_UseTime(self.Font) .. "\nreverting to " .. self.UsedFont)
+ end
+ end
+end
+
+function PART:SetCustomFont(str)
+ self.CustomFont = str
+ local buildable, reason = self:CheckFontBuildability(str)
+ --suppress if not requesting custom font, and if name is too long
+ if buildable then self:TryCreateFont() else return end
+ if self:GetPlayerOwner() == pac.LocalPlayer then
+ if not self.pace_properties then return end
+ if pace.current_part == self then
+ local pnl = self["pac_property_label_CustomFont"]
+ if IsValid(pnl) then
+ if not included_fonts[str] and not default_fonts[str] then
+ --pnl:SetValue("")
+ pnl:Clear()
+ pnl:CreateAlternateLabel("bad font", true)
+ pnl:SetTooltip(GetReasonBadFont_At_CreationTime(str))
+ else
+ pace.PopulateProperties(self)
end
- timer.Simple(4, function()
- if not pcall(surface_SetFont, str) then
- pac.Message(Color(255,150,0),str.." Font still not found! Reverting to DermaDefault!")
- str = "DermaDefault"
- self.UsedFont = str
- end
- end)
- else
- timer.Simple(5, function()
- if not pcall(surface_SetFont, str) then
- pac.Message(Color(255,150,0),str.." Font still not found! Reverting to DermaDefault!")
- str = "DermaDefault"
- self.UsedFont = str
- end
- end)
end
end
+
end
- self.Font = self.UsedFont
end
-local lastfontcreationtime = 0
+
+function PART:GetBrackets()
+ local bracket1 = ""
+ local bracket2 = ""
+ local bracks = tostring(self.VectorBrackets)
+ if #bracks % 2 == 1 then
+ bracket1 = string.sub(bracks,1, (#bracks + 1) / 2) or ""
+ bracket2 = string.sub(bracks, (#bracks + 1) / 2, #bracks) or ""
+ else
+ bracket1 = string.sub(bracks,1, #bracks / 2) or ""
+ bracket2 = string.sub(bracks, #bracks / 2 + 1, #bracks) or ""
+ end
+ return bracket1, bracket2
+end
+
+function PART:GetNiceVector(vec)
+ local bracket1, bracket2 = self:GetBrackets()
+ return bracket1..math.Round(vec.x,self.RoundingPosition)..self.VectorSeparator..math.Round(vec.y,self.RoundingPosition)..self.VectorSeparator..math.Round(vec.z,self.RoundingPosition)..bracket2
+end
+
+
function PART:OnDraw()
local pos, ang = self:GetDrawPosition()
+
self:CheckFont()
- if not pcall(surface_SetFont, self.UsedFont) then return end
+ if not self.UsedFont then self.UsedFont = self.Font end
+ if not usable_fonts[self.UsedFont] then return end
local DisplayText = self.Text or ""
if self.TextOverride == "Text" then goto DRAW end
@@ -278,15 +492,13 @@ function PART:OnDraw()
DisplayText = math.Round(ent:GetVelocity():Length(),2)
elseif self.TextOverride == "VelocityVector" then
local ent = self:GetOwner() or self:GetRootPart():GetOwner()
- local vec = ent:GetVelocity()
- DisplayText = "("..math.Round(vec.x,self.RoundingPosition)..","..math.Round(vec.y,self.RoundingPosition)..","..math.Round(vec.z,self.RoundingPosition)..")"
+ DisplayText = self:GetNiceVector(ent:GetVelocity())
elseif self.TextOverride == "PositionVector" then
local vec = self:GetDrawPosition()
- DisplayText = "("..math.Round(vec.x,self.RoundingPosition)..","..math.Round(vec.y,self.RoundingPosition)..","..math.Round(vec.z,self.RoundingPosition)..")"
+ DisplayText = self:GetNiceVector(vec)
elseif self.TextOverride == "OwnerPositionVector" then
local ent = self:GetRootPart():GetOwner()
- local vec = ent:GetPos()
- DisplayText = "("..math.Round(vec.x,self.RoundingPosition)..","..math.Round(vec.y,self.RoundingPosition)..","..math.Round(vec.z,self.RoundingPosition)..")"
+ DisplayText = self:GetNiceVector(ent:GetPos())
elseif self.TextOverride == "SequenceName" then
DisplayText = self:GetRootPart():GetOwner():GetSequenceName(self:GetPlayerOwner():GetSequence())
elseif self.TextOverride == "PlayerName" then
@@ -366,6 +578,36 @@ function PART:OnDraw()
else DisplayText = "not driving" end
elseif self.TextOverride == "Proxy" then
DisplayText = ""..math.Round(self.DynamicTextValue,self.RoundingPosition)
+ elseif self.TextOverride == "ProxyVector" then
+ DisplayText = self:GetNiceVector(self.DynamicVectorValue)
+ elseif self.TextOverride == "ChatTyping" then
+ if self:GetPlayerOwner() == pac.LocalPlayer and not pac.broadcast_chat_typing then
+ pac.AddHook("ChatTextChanged", "broadcast_chat_typing", function(text)
+ net.Start("pac_chat_typing_mirror")
+ net.WriteString(text)
+ net.SendToServer()
+ end)
+ pac.AddHook("FinishChat", "end_chat_typing", function(text)
+ net.Start("pac_chat_typing_mirror")
+ net.WriteString("")
+ net.SendToServer()
+ end)
+ pac.broadcast_chat_typing = true
+ end
+ DisplayText = self:GetPlayerOwner().pac_mirrored_chat_text or ""
+ elseif self.TextOverride == "ChatSent" then
+ if self:GetPlayerOwner().pac_say_event then
+ DisplayText = self:GetPlayerOwner().pac_say_event.str
+ else
+ DisplayText = ""
+ end
+ end
+
+ if not string.find(self.TextOverride, "Vector") then
+ if self.UseBracketsOnNonVectors then
+ local bracket1, bracket2 = self:GetBrackets()
+ DisplayText = bracket1 .. DisplayText .. bracket2
+ end
end
if self.ConcatenateTextAndOverrideValue then
@@ -377,8 +619,49 @@ function PART:OnDraw()
end
::DRAW::
+ if self.Truncate then
+
+ if self.TruncateSkipCharacters_tbl then
+ local temp_string = ""
+ local char_pos = 1
+ local temp_chunk = ""
+ for i=1,#DisplayText,1 do
+ local char = DisplayText[i]
+ local escaped_char = false
+ if char == "\n" or char == "\t" then escaped_char = true end
+ if not self.TruncateWords then --char by char, add to the string only if it's a non-skip character
+ if char_pos > self.TruncatePosition then break end
+ if self.TruncateSkipCharacters_tbl[char] or escaped_char then
+ temp_chunk = temp_chunk .. char
+ else
+ temp_string = temp_string .. temp_chunk .. char
+ temp_chunk = ""
+ char_pos = char_pos + 1
+ end
+ else --word by word, add to the string once i reaches the end or reaches a boundary
+ if char_pos > self.TruncatePosition then break end
+ if not self.TruncateSkipCharacters_tbl[char] and (self.TruncateSkipCharacters_tbl[DisplayText[i+1]] or i == #DisplayText) then
+ temp_string = string.sub(DisplayText,0,i)
+ char_pos = char_pos + 1
+ end
+ end
+ end
+ DisplayText = temp_string
+ else
+ DisplayText = string.sub(DisplayText, 0, self.TruncatePosition)
+ end
+ end
+
+ if self.SentenceNewlines then
+ DisplayText = string.Replace(DisplayText,". ",".\n")
+ DisplayText = string.Replace(DisplayText,"! ","!\n")
+ DisplayText = string.Replace(DisplayText,"? ","?\n")
+ end
if DisplayText ~= "" then
+ local w, h = surface.GetTextSize(DisplayText)
+ if not w or not h then return end
+
if self.DrawMode == "DrawTextOutlined" then
cam_Start3D(EyePos(), EyeAngles())
cam_Start3D2D(pos, ang, self.Size)
@@ -406,16 +689,17 @@ function PART:OnDraw()
DisableClipping(oldState)
cam_End3D2D()
cam_End3D()
- elseif self.DrawMode == "SurfaceText" or self.DrawMode == "DrawTextOutlined2D" then
- hook.Add("HUDPaint", "pac.DrawText"..self.UniqueID, function()
- if not pcall(surface_SetFont, self.UsedFont) then return end
- self:SetFont(self.UsedFont)
+ elseif self.DrawMode == "SurfaceText" or self.DrawMode == "DrawTextOutlined2D" or self.DrawMode == "DrawDrawText" then
+ pac.AddHook("HUDPaint", "pac.DrawText"..self.UniqueID, function()
+ surface.SetFont(self.UsedFont)
surface.SetTextColor(self.Color.r, self.Color.g, self.Color.b, 255*self.Alpha)
- surface.SetFont(self.UsedFont)
+
local pos2d = self:GetDrawPosition():ToScreen()
+ local pos2d_original = table.Copy(pos2d)
local w, h = surface.GetTextSize(DisplayText)
+ if not h or not w then return end
if self.HorizontalTextAlign == 0 then --left
pos2d.x = pos2d.x
@@ -434,17 +718,11 @@ function PART:OnDraw()
end
surface.SetTextPos(pos2d.x, pos2d.y)
- local dist = (EyePos() - self:GetWorldPosition()):Length()
- local fadestartdist = 200
- local fadeenddist = 1000
- if fadestartdist == 0 then fadestartdist = 0.1 end
- if fadeenddist == 0 then fadeenddist = 0.1 end
-
- if fadestartdist > fadeenddist then
- local temp = fadestartdist
- fadestartdist = fadeenddist
- fadeenddist = temp
- end
+ local dist = (pac.EyePos - self:GetWorldPosition()):Length()
+
+ --clamp down the part's requested values with the viewer client's cvar
+ local fadestartdist = math.max(draw_distance:GetInt() - 200,0)
+ local fadeenddist = math.max(draw_distance:GetInt(),0)
if dist < fadeenddist then
if dist < fadestartdist then
@@ -452,16 +730,18 @@ function PART:OnDraw()
draw.SimpleTextOutlined(DisplayText, self.UsedFont, pos2d.x, pos2d.y, Color(self.Color.r,self.Color.g,self.Color.b,255*self.Alpha), TEXT_ALIGN_TOP, TEXT_ALIGN_LEFT, self.Outline, Color(self.OutlineColor.r,self.OutlineColor.g,self.OutlineColor.b, 255*self.OutlineAlpha))
elseif self.DrawMode == "SurfaceText" then
surface.DrawText(DisplayText, self.ForceAdditive)
+ elseif self.DrawMode == "DrawDrawText" then
+ draw.DrawText(DisplayText, self.UsedFont, pos2d_original.x, pos2d.y, Color(self.Color.r,self.Color.g,self.Color.b,255*self.Alpha), self.HorizontalTextAlign)
end
-
else
- local fade = math.pow(math.Clamp(1 - (dist-fadestartdist)/fadeenddist,0,1),3)
-
+ local fade = math.pow(math.Clamp(1 - (dist-fadestartdist)/math.max(fadeenddist - fadestartdist,0.1),0,1),3)
if self.DrawMode == "DrawTextOutlined2D" then
draw.SimpleTextOutlined(DisplayText, self.UsedFont, pos2d.x, pos2d.y, Color(self.Color.r,self.Color.g,self.Color.b,255*self.Alpha*fade), TEXT_ALIGN_TOP, TEXT_ALIGN_LEFT, self.Outline, Color(self.OutlineColor.r,self.OutlineColor.g,self.OutlineColor.b, 255*self.OutlineAlpha*fade))
elseif self.DrawMode == "SurfaceText" then
surface.SetTextColor(self.Color.r * fade, self.Color.g * fade, self.Color.b * fade)
surface.DrawText(DisplayText, true)
+ elseif self.DrawMode == "DrawDrawText" then
+ draw.DrawText(DisplayText, self.UsedFont, pos2d.x, pos2d.y, Color(self.Color.r,self.Color.g,self.Color.b,255*self.Alpha*fade), TEXT_ALIGN_LEFT)
end
end
@@ -469,54 +749,50 @@ function PART:OnDraw()
end)
end
if self.DrawMode == "DrawTextOutlined" then
- hook.Remove("HUDPaint", "pac.DrawText"..self.UniqueID)
+ pac.RemoveHook("HUDPaint", "pac.DrawText"..self.UniqueID)
end
- else hook.Remove("HUDPaint", "pac.DrawText"..self.UniqueID) end
+ else pac.RemoveHook("HUDPaint", "pac.DrawText"..self.UniqueID) end
end
function PART:Initialize()
+ if self.Font == "default" then self.Font = "DermaDefault" end
self:TryCreateFont()
+ self.anotherwarning = false
end
-function PART:CheckFont()
- if self.CreateCustomFont then
- lastfontcreationtime = lastfontcreationtime or 0
- if lastfontcreationtime + 3 <= CurTime() then
- self:TryCreateFont()
- end
- else
- self:SetFont(self.Font)
- end
-end
-
-function PART:TryCreateFont()
- if "Font_"..self.CustomFont.."_"..math.Round(self.FontSize,3).."_"..self.UniqueID == self.lastcustomfont then
- self.UsedFont = "Font_"..self.CustomFont.."_"..math.Round(self.FontSize,3).."_"..self.UniqueID
- return
- end
+function PART:TryCreateFont(force_refresh)
+ local newfont = "Font_"..self.CustomFont.."_"..math.Round(self.FontSize,3).."_"..self.UniqueID
if self.CreateCustomFont then
- local newfont = "Font_"..self.CustomFont.."_"..math.Round(self.FontSize,3).."_"..self.UniqueID
+ if usable_fonts[newfont] then self.UsedFont = newfont self.Font = newfont return end
+ local buildable, reason = self:CheckFontBuildability(self.CustomFont)
+ --if reason == "default font" then self.CustomFont = "default" end
+ if not buildable then
+ return
+ end
+ if lastfontcreationtime + 2 > CurTime() then return end
surface.CreateFont( newfont, {
font = self.CustomFont, -- Use the font-name which is shown to you by your operating system Font Viewer, not the file name
extended = self.Extended,
size = self.FontSize,
- weight = self.Weight,
- blursize = self.BlurSize,
- scanlines = self.ScanLines,
- antialias = self.Antialias,
- underline = self.Underline,
- italic = self.Italic,
- strikeout = self.Strikeout,
- symbol = self.Symbol,
- rotary = self.Rotary,
+ weight = self.FontWeight,
+ blursize = self.FontBlurSize,
+ scanlines = self.FontScanLines,
+ antialias = self.FontAntialias,
+ underline = self.FontUnderline,
+ italic = self.FontItalic,
+ strikeout = self.FontStrikeout,
+ symbol = self.FontSymbol,
+ rotary = self.FontRotary,
shadow = self.Shadow,
- additive = self.Additive,
+ additive = self.FontAdditive,
outline = self.Outline,
} )
- self:SetFont(newfont)
- self.lastcustomfont = newfont
lastfontcreationtime = CurTime()
+ --base fonts are ok to derive from
+ usable_fonts[newfont] = true
+ self.UsedFont = newfont
+ self.Font = newfont
end
end
@@ -525,10 +801,23 @@ function PART:OnShow()
end
function PART:OnHide()
- hook.Remove("HUDPaint", "pac.DrawText"..self.UniqueID)
+ pac.RemoveHook("HUDPaint", "pac.DrawText"..self.UniqueID)
end
+
function PART:OnRemove()
- hook.Remove("HUDPaint", "pac.DrawText"..self.UniqueID)
+ pac.RemoveHook("HUDPaint", "pac.DrawText"..self.UniqueID)
+ local remains_chat_text_part = false
+ for i,v in ipairs(pac.GetLocalParts()) do
+ if v.ClassName == "text" then
+ if v.TextOverride == "ChatTyping" then
+ remains_chat_text_part = true
+ end
+ end
+ end
+ if not remains_chat_text_part then
+ pac.RemoveHook("ChatTextChanged", "broadcast_chat_typing")
+ pac.broadcast_chat_typing = false
+ end
end
function PART:SetText(str)
self.Text = str
diff --git a/lua/pac3/core/client/test.lua b/lua/pac3/core/client/test.lua
index 200f82616..b88b7d587 100644
--- a/lua/pac3/core/client/test.lua
+++ b/lua/pac3/core/client/test.lua
@@ -79,17 +79,17 @@ local function start_test(name, done)
end
function test.Setup()
- hook.Add("ShouldDrawLocalPlayer", "pac_test", function() return true end)
+ pac.AddHook("ShouldDrawLocalPlayer", "test", function() return true end)
end
function test.Teardown()
- hook.Remove("ShouldDrawLocalPlayer", "pac_test")
+ pac.RemoveHook("ShouldDrawLocalPlayer", "test")
end
function test.Run(done) error("test.Run is not defined") end
function test.Remove()
- hook.Remove("ShouldDrawLocalPlayer", "pac_test")
- hook.Remove("Think", "pac_test_coroutine")
+ pac.RemoveHook("ShouldDrawLocalPlayer", "test")
+ pac.RemoveHook("Think", "test_coroutine")
if test.done then return end
@@ -172,7 +172,7 @@ local function start_test(name, done)
test.Remove()
end
- hook.Add("Think", "pac_test_coroutine", function()
+ pac.AddHook("Think", "test_coroutine", function()
if not test.co then return end
local ok, err = coroutine.resume(test.co)
@@ -213,7 +213,7 @@ concommand.Add("pac_test", function(ply, _, args)
local current_test = nil
- hook.Add("Think", "pac_tests", function()
+ pac.AddHook("Think", "tests", function()
if current_test then
if current_test.time < os.clock() then
msg_error("test ", current_test.name, " timed out")
@@ -232,7 +232,7 @@ concommand.Add("pac_test", function(ply, _, args)
local name = table.remove(tests, 1)
if not name then
msg("finished testing")
- hook.Remove("Think", "pac_tests")
+ pac.RemoveHook("Think", "tests")
return
end
diff --git a/lua/pac3/core/client/util.lua b/lua/pac3/core/client/util.lua
index 996eea451..b032a2df3 100644
--- a/lua/pac3/core/client/util.lua
+++ b/lua/pac3/core/client/util.lua
@@ -364,27 +364,24 @@ do -- hook helpers
pac.added_hooks = pac.added_hooks or {}
function pac.AddHook(event_name, id, func, priority)
- id = "pac_" .. id
+ id = isstring(id) and "pac_" .. id or id
if not DLib and not ULib then
priority = nil
end
-
if pac.IsEnabled() then
hook.Add(event_name, id, func, priority)
+ pac.added_hooks[event_name .. tostring(id)] = {event_name = event_name, id = id, func = func, priority = priority}
end
-
- pac.added_hooks[event_name .. id] = {event_name = event_name, id = id, func = func, priority = priority}
end
function pac.RemoveHook(event_name, id)
- id = "pac_" .. id
+ id = "pac_" .. tostring(id)
local data = pac.added_hooks[event_name .. id]
if data then
hook.Remove(data.event_name, data.id)
-
pac.added_hooks[event_name .. id] = nil
end
end
@@ -395,13 +392,21 @@ do -- hook helpers
function pac.EnableAddedHooks()
for _, data in pairs(pac.added_hooks) do
- hook.Add(data.event_name, data.id, data.func, data.priority)
+ if ispanel(data.id) and not IsValid(data.id) then -- Panels can be NULL and are (probably) already removed
+ pac.added_hooks[data.event_name .. tostring(data.id)] = nil
+ else
+ hook.Add(data.event_name, data.id, data.func, data.priority)
+ end
end
end
function pac.DisableAddedHooks()
for _, data in pairs(pac.added_hooks) do
- hook.Remove(data.event_name, data.id)
+ if ispanel(data.id) and not IsValid(data.id) then -- Panels can be NULL and are already removed
+ pac.added_hooks[data.event_name .. tostring(data.id)] = nil
+ else
+ hook.Remove(data.event_name, data.id)
+ end
end
end
end
diff --git a/lua/pac3/core/server/event.lua b/lua/pac3/core/server/event.lua
index 84cc0db15..9460a933e 100644
--- a/lua/pac3/core/server/event.lua
+++ b/lua/pac3/core/server/event.lua
@@ -1,10 +1,16 @@
util.AddNetworkString("pac_proxy")
util.AddNetworkString("pac_event")
+util.AddNetworkString("pac_event_set_sequence")
+util.AddNetworkString("pac_event_define_sequence_bounds")
+util.AddNetworkString("pac_event_update_sequence_bounds")
-- event
concommand.Add("pac_event", function(ply, _, args)
- if not args[1] then return end
+ if args[1] == nil then
+ ply:PrintMessage(HUD_PRINTCONSOLE, "\npac_event needs at least one argument.\nname: any name, preferably without spaces\nmode: a number.\n\t0 turns off\n\t1 turns on\n\t2 toggles on/off\n\twithout a second argument, the event is a single-shot\n\ne.g. pac_event light 2\n")
+ return
+ end
local event = args[1]
local extra = tonumber(args[2]) or 0
@@ -16,15 +22,124 @@ concommand.Add("pac_event", function(ply, _, args)
extra = ply.pac_event_toggles[event] and 1 or 0
end
+ if args[2] == "random" then
+ local min = tonumber(args[3])
+ local max = tonumber(args[4])
+ if isnumber(min) and isnumber(max) then
+ local append = math.floor(math.Rand(tonumber(args[3]), tonumber(args[4]) + 1))
+ event = event .. append
+ end
+ end
+
net.Start("pac_event", true)
net.WriteEntity(ply)
net.WriteString(event)
net.WriteInt(extra, 8)
net.Broadcast()
+ ply.pac_command_events = ply.pac_command_events or {}
+ ply.pac_command_events[event] = ply.pac_command_events[event] or {}
+ ply.pac_command_events[event] = {name = event, time = pac.RealTime, on = extra}
+end, nil,
+[[pac_event triggers command events. it needs at least one argument.
+name: any name, preferably without spaces
+modes:
+ 0 turns off
+ 1 turns on
+ 2 toggles on/off
+ without a second argument, the event is a single-shot
+ random followed by two numbers will run a single-shot randomly, the name will be the base name with the number at the end
+
+e.g.
+ pac_event light 2
+ pac_event attack random 1 3]])
+
+net.Receive("pac_event_define_sequence_bounds", function(len, ply)
+ local bounds = net.ReadTable()
+ if bounds == nil then return end
+ for event,tbl in pairs(bounds) do
+ ply.pac_command_event_sequencebases = ply.pac_command_event_sequencebases or {}
+ local current_seq_value = 1
+ if ply.pac_command_event_sequencebases[event] then
+ if ply.pac_command_event_sequencebases[event].current then
+ current_seq_value = ply.pac_command_event_sequencebases[event].current
+ end
+ end
+ ply.pac_command_event_sequencebases[event] = {name = event, min = tbl[1], max = tbl[2], current = current_seq_value}
+ end
+ net.Start("pac_event_update_sequence_bounds") net.WriteEntity(ply) net.WriteTable(bounds) net.Broadcast()
+end)
+
+concommand.Add("pac_event_sequenced_force_set_bounds", function(ply, cmd, args)
+ if args[1] == nil then return end
+ local event = args[1]
+ local min = args[2]
+ local max = args[3]
+ ply.pac_command_event_sequencebases = ply.pac_command_event_sequencebases or {}
+ ply.pac_command_event_sequencebases[event] = {name = event, min = tonumber(min), max = tonumber(max), current = 0}
+ net.Start("pac_event_update_sequence_bounds") net.WriteEntity(ply) net.WriteTable(ply.pac_command_event_sequencebases) net.Broadcast()
+end)
+
+concommand.Add("pac_event_sequenced", function(ply, cmd, args)
+ if args[1] == nil then
+ ply:PrintMessage(HUD_PRINTCONSOLE, "\npac_event_sequenced needs at least one* argument.\nname: the base name of your series of events\naction: set, forward, backward\nnumber: if using the set mode, the number to set\n\ne.g. pac_event_sequenced hat_style set 3\n")
+ return
+ end
+
+ local event = args[1]
+ local action = args[2] or "+"
+ local sequence_number = 0
+ local set_target = tonumber(args[3]) or 1
+
+ ply.pac_command_events = ply.pac_command_events or {}
+
+ local data
+ if ply.pac_command_event_sequencebases ~= nil then
+ if ply.pac_command_event_sequencebases[event] == nil then ply.pac_command_event_sequencebases[event] = {name = event, min = 1, max = 1} end
+ else
+ ply.pac_command_event_sequencebases = {}
+ ply.pac_command_event_sequencebases[event] = {name = event, min = 1, max = 1}
+ end
+
+ data = ply.pac_command_event_sequencebases[event]
+ sequence_number = data.current
+
+ local target_number = 1
+ local min = data.min
+ local max = data.max
+
+ sequence_number = tonumber(data.current) or 1
+ if action == "+" or action == "forward" or action == "add" or action == "sequence+" or action == "advance" then
+ if sequence_number >= max then
+ target_number = min
+ else target_number = sequence_number + 1 end
+ data.current = target_number
+
+ elseif action == "-" or action == "backward" or action == "sub" or action == "sequence-" then
+ if sequence_number <= min then
+ target_number = max
+ else target_number = sequence_number - 1 end
+ data.current = target_number
+
+ elseif action == "set" then
+ sequence_number = set_target or 1
+ target_number = set_target
+ data.current = target_number
+ else
+ ply:PrintMessage(HUD_PRINTCONSOLE, "\npac_event_sequenced : wrong action name. Valid action names are:\nforward, +, add, sequence+ or advance\nbackward, -, sub, or sequence-\nset")
+ return
+ end
+ net.Start("pac_event_set_sequence")
+ net.WriteEntity(ply)
+ net.WriteString(event)
+ net.WriteUInt(target_number,8)
+ net.Broadcast()
end)
concommand.Add("+pac_event", function(ply, _, args)
- if not args[1] then return end
+ if not args[1] then
+ ply:PrintMessage(HUD_PRINTCONSOLE, "+pac_event needs a name argument, and the toggling argument. e.g. +pac_event hold_light 2\nwithout the toggling arg, implicitly adds _on to the command name, like running \"pac_event name_on\", and \"pac_event name_off\" when released")
+ return
+ end
if args[2] == "2" or args[2] == "toggle" then
local event = args[1]
@@ -42,10 +157,13 @@ concommand.Add("+pac_event", function(ply, _, args)
net.WriteString(args[1] .. "_on")
net.Broadcast()
end
-end)
+end, nil, "activates a command event. more effective when bound. \ne.g. \"+pac_event name\" will run \"pac_event name_on\" when the button is held, \"pac_event name_off\" when the button is held. Take note these are instant commands, they would need a command event with duration.\nmeanwhile, \"+pac_event name 2\" will run \"pac_event name 1\" when the button is held, \"pac_event name 0\" when the button is held. Take note these are held commands.")
concommand.Add("-pac_event", function(ply, _, args)
- if not args[1] then return end
+ if not args[1] then
+ ply:PrintMessage(HUD_PRINTCONSOLE, "-pac_event needs a name argument, and the toggling argument. e.g. +pac_event hold_light 2\nwithout the toggling arg, implicitly adds _on to the command name, like running \"pac_event name_off\"")
+ return
+ end
if args[2] == "2" or args[2] == "toggle" then
local event = args[1]
@@ -66,12 +184,56 @@ end)
-- proxy
concommand.Add("pac_proxy", function(ply, _, args)
+ str = args[1]
+ if args[1] == nil then
+ ply:PrintMessage(HUD_PRINTCONSOLE, "\npac_proxy needs at least two arguments.\nname\nnumber, or a series of numbers for a vector. increment notation is available, such as ++1 or --1.\ne.g. pac_proxy myvector 1 2 3\ne.g. pac_proxy value ++5")
+ return
+ end
+
+ if ply:IsValid() then
+ ply.pac_proxy_events = ply.pac_proxy_events or {}
+ end
+ local x
+ local y
+ local z
+ if ply.pac_proxy_events[str] ~= nil then
+ if args[2] then
+ if string.sub(args[2],1,2) == "++" or string.sub(args[2],1,2) == "--" then
+ x = ply.pac_proxy_events[str].x + tonumber(string.sub(args[2],2,#args[2]))
+ else x = tonumber(args[2]) or ply.pac_proxy_events[str].x or 0 end
+ end
+
+ if args[3] then
+ if string.sub(args[3],1,2) == "++" or string.sub(args[3],1,2) == "--" then
+ y = ply.pac_proxy_events[str].y + tonumber(string.sub(args[3],2,#args[3]))
+ else y = tonumber(args[3]) or ply.pac_proxy_events[str].y or 0 end
+ end
+ if not args[3] then y = 0 end
+
+ if args[4] then
+ if string.sub(args[4],1,2) == "++" or string.sub(args[4],1,2) == "--" then
+ z = ply.pac_proxy_events[str].z + tonumber(string.sub(args[4],2,#args[4]))
+ else z = tonumber(args[4]) or ply.pac_proxy_events[str].z or 0 end
+ end
+ if not args[4] then z = 0 end
+ else
+ x = tonumber(args[2]) or 0
+ y = tonumber(args[3]) or 0
+ z = tonumber(args[4]) or 0
+ end
+ ply.pac_proxy_events[str] = {name = str, x = x, y = y, z = z}
+
net.Start("pac_proxy", true)
net.WriteEntity(ply)
net.WriteString(args[1])
- net.WriteFloat(tonumber(args[2]) or 0)
- net.WriteFloat(tonumber(args[3]) or 0)
- net.WriteFloat(tonumber(args[4]) or 0)
+ net.WriteFloat(x or 0)
+ net.WriteFloat(y or 0)
+ net.WriteFloat(z or 0)
net.Broadcast()
-end)
+end, nil,
+[[pac_proxy sets the number of a command function in a proxy. it is typically accessed as command(\"value\") needs at least two arguments.
+name: any name, preferably without spaces
+numbers: a number, or a series of numbers for a vector. increment notation is available, such as ++1 and --1.
+e.g. pac_proxy myvector 1 2 3
+e.g. pac_proxy value ++5]])
diff --git a/lua/pac3/core/server/in_skybox.lua b/lua/pac3/core/server/in_skybox.lua
index 35d4c54a3..01f214272 100644
--- a/lua/pac3/core/server/in_skybox.lua
+++ b/lua/pac3/core/server/in_skybox.lua
@@ -1,4 +1,4 @@
-hook.Add("InitPostEntity","pac_get_sky_camera",function()
+pac.AddHook("InitPostEntity", "get_sky_camera", function()
local sky_camera = ents.FindByClass("sky_camera")[1]
if sky_camera then
local nwVarName = "pac_in_skybox"
diff --git a/lua/pac3/core/server/init.lua b/lua/pac3/core/server/init.lua
index 4e60b8688..eac2c9fda 100644
--- a/lua/pac3/core/server/init.lua
+++ b/lua/pac3/core/server/init.lua
@@ -7,8 +7,8 @@ pac.resource = include("pac3/libraries/resource.lua")
CreateConVar("has_pac3", "1", {FCVAR_NOTIFY})
CreateConVar("pac_allow_blood_color", "1", {FCVAR_NOTIFY}, "Allow to use custom blood color")
-CreateConVar('pac_allow_mdl', '1', CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Allow to use custom MDLs')
-CreateConVar('pac_allow_mdl_entity', '1', CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, 'Allow to use custom MDLs as Entity')
+CreateConVar("pac_allow_mdl", "1", CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow to use custom MDLs")
+CreateConVar("pac_allow_mdl_entity", "1", CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow to use custom MDLs as Entity")
include("util.lua")
@@ -20,4 +20,4 @@ include("net_messages.lua")
include("test_suite_backdoor.lua")
include("in_skybox.lua")
-hook.Run("pac_Initialized")
+pac.CallHook("Initialized")
diff --git a/lua/pac3/core/server/net_messages.lua b/lua/pac3/core/server/net_messages.lua
index b0bfd379e..3a22e417f 100644
--- a/lua/pac3/core/server/net_messages.lua
+++ b/lua/pac3/core/server/net_messages.lua
@@ -1,6 +1,10 @@
util.AddNetworkString("pac.AllowPlayerButtons")
util.AddNetworkString("pac.BroadcastPlayerButton")
+util.AddNetworkString("pac_chat_typing_mirror")
+util.AddNetworkString("pac_chat_typing_mirror_broadcast")
+util.AddNetworkString("pac_fire_bullets_for_singleplayer")
+util.AddNetworkString("pac_hide_bullets_get")
do -- button event
net.Receive("pac.AllowPlayerButtons", function(length, client)
@@ -28,4 +32,24 @@ do -- button event
pac.AddHook("PlayerButtonUp", "event", function(ply, key)
broadcast_key(ply, key, false)
end)
+end
+
+net.Receive("pac_chat_typing_mirror", function(len, ply)
+ local str = net.ReadString()
+ net.Start("pac_chat_typing_mirror_broadcast")
+ net.WriteString(str)
+ net.WriteEntity(ply)
+ net.Broadcast()
+end)
+
+if game.SinglePlayer() then
+ hook.Add("EntityFireBullets", "pac_bullet_singleplayer_hack", function(ent, data)
+ if ent:IsPlayer() then
+ net.Start("pac_fire_bullets_for_singleplayer") net.WriteEntity(ent) net.WriteUInt(game.GetAmmoID(data.AmmoType),8) net.Broadcast()
+ end
+ if ent:GetNWBool("pac_hide_bullets", false) then return false end
+ end)
+ net.Receive("pac_hide_bullets_get", function(len, ply)
+ ply:SetNWBool("pac_hide_bullets",net.ReadBool())
+ end)
end
\ No newline at end of file
diff --git a/lua/pac3/core/server/util.lua b/lua/pac3/core/server/util.lua
index 4e8918a62..923f340e4 100644
--- a/lua/pac3/core/server/util.lua
+++ b/lua/pac3/core/server/util.lua
@@ -32,7 +32,7 @@ function pac.AddHook(str, id, func, priority)
local status, a, b, c, d, e, f, g = pcall(func, ...)
if not status then
- pac.Message('Error on hook ' .. str .. ' (' .. id .. ')! ', a)
+ pac.Message("Error on hook " .. str .. " (" .. id .. ")! ", a)
return
end
diff --git a/lua/pac3/core/shared/entity_mutator.lua b/lua/pac3/core/shared/entity_mutator.lua
index 77f20e4b3..6b03d57f6 100644
--- a/lua/pac3/core/shared/entity_mutator.lua
+++ b/lua/pac3/core/shared/entity_mutator.lua
@@ -301,15 +301,15 @@ if SERVER then
end
end
- hook.Add("PlayerInitialSpawn", "pac_entity_mutators_spawn", function(ply)
- local id = "pac_entity_mutators_spawn" .. ply:UniqueID()
- hook.Add( "SetupMove", id, function(movingPly, _, cmd)
+ pac.AddHook("PlayerInitialSpawn", "entity_mutators_spawn", function(ply)
+ local id = "entity_mutators_spawn" .. ply:UniqueID()
+ pac.AddHook( "SetupMove", id, function(movingPly, _, cmd)
if not ply:IsValid() then
- hook.Remove("SetupMove", id)
+ pac.RemoveHook("SetupMove", id)
elseif movingPly == ply and not cmd:IsForced() then
emut.ReplicateMutatorsForPlayer(ply)
- hook.Remove("SetupMove", id)
+ pac.RemoveHook("SetupMove", id)
end
end)
end)
@@ -323,7 +323,7 @@ function emut.RemoveMutationsForPlayer(ply)
end
end
-hook.Add("EntityRemoved", "pac_entity_mutators_left", function(ent)
+pac.AddHook("EntityRemoved", "entity_mutators_left", function(ent)
if not IsValid(ent) then return end
if ent:IsPlayer() then
if Player(ent:UserID()) == NULL then
diff --git a/lua/pac3/core/shared/entity_mutators/draw_shadow.lua b/lua/pac3/core/shared/entity_mutators/draw_shadow.lua
new file mode 100644
index 000000000..919bed31a
--- /dev/null
+++ b/lua/pac3/core/shared/entity_mutators/draw_shadow.lua
@@ -0,0 +1,24 @@
+local MUTATOR = {}
+
+MUTATOR.ClassName = "draw_shadow"
+
+function MUTATOR:WriteArguments(b)
+ net.WriteBool(b)
+end
+
+function MUTATOR:ReadArguments()
+ return net.ReadBool()
+end
+
+if SERVER then
+ function MUTATOR:StoreState()
+ return self.Entity.pac_emut_draw_shadow
+ end
+
+ function MUTATOR:Mutate(b)
+ self.Entity:DrawShadow(b)
+ self.Entity.pac_emut_draw_shadow = b
+ end
+end
+
+pac.emut.Register(MUTATOR)
\ No newline at end of file
diff --git a/lua/pac3/core/shared/footsteps_fix.lua b/lua/pac3/core/shared/footsteps_fix.lua
index 8440073a2..c38ac2382 100644
--- a/lua/pac3/core/shared/footsteps_fix.lua
+++ b/lua/pac3/core/shared/footsteps_fix.lua
@@ -1,11 +1,11 @@
if game.SinglePlayer() then
if SERVER then
- util.AddNetworkString('pac_footstep')
- util.AddNetworkString('pac_footstep_request_state_update')
- util.AddNetworkString('pac_signal_mute_footstep')
+ util.AddNetworkString("pac_footstep")
+ util.AddNetworkString("pac_footstep_request_state_update")
+ util.AddNetworkString("pac_signal_mute_footstep")
- hook.Add("PlayerFootstep", "footstep_fix", function(ply, pos, _, snd, vol)
+ pac.AddHook("PlayerFootstep", "footstep_fix", function(ply, pos, _, snd, vol)
net.Start("pac_footstep_request_state_update")
net.Send(ply)
@@ -17,14 +17,14 @@ if game.SinglePlayer() then
net.Broadcast()
end)
- net.Receive("pac_signal_mute_footstep", function(len,ply)
+ net.Receive("pac_signal_mute_footstep", function(len, ply)
local b = net.ReadBool()
ply.pac_mute_footsteps = b
if ply.pac_mute_footsteps then
- hook.Add("PlayerFootstep", "pac_footstep_silence", function()
+ pac.AddHook("PlayerFootstep", "footstep_silence", function()
return b
end)
- else hook.Remove("PlayerFootstep", "pac_footstep_silence") end
+ else pac.RemoveHook("PlayerFootstep", "footstep_silence") end
end)
diff --git a/lua/pac3/core/shared/movement.lua b/lua/pac3/core/shared/movement.lua
index f0052f22a..6967b8f56 100644
--- a/lua/pac3/core/shared/movement.lua
+++ b/lua/pac3/core/shared/movement.lua
@@ -1,24 +1,31 @@
local movementConvar = CreateConVar("pac_free_movement", -1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "allow players to modify movement. -1 apply only allow when noclip is allowed, 1 allow for all gamemodes, 0 to disable")
+local allowMass = CreateConVar("pac_player_movement_allow_mass", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "enables changing player mass in player movement. 1 to enable, 0 to disable", 0, 1)
+local massUpperLimit = CreateConVar("pac_player_movement_max_mass", 50000, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "restricts the maximum mass that players can use with player movement", 85, 50000)
+local massLowerLimit = CreateConVar("pac_player_movement_min_mass", 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "restricts the minimum mass that players can use with player movement", 0, 85)
+local massDamageScale = CreateConVar("pac_player_movement_physics_damage_scaling", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "restricts the damage scaling applied to players by modified mass values. 1 to enable, 0 to disable", 0, 1)
local default = {
JumpHeight = 200,
StickToGround = true,
GroundFriction = 0.12,
AirFriction = 0.01,
+ HorizontalAirFrictionMultiplier = 1,
+ StrafingStrengthMultiplier = 1,
Gravity = Vector(0,0,-600),
+ Mass = 85,
Noclip = false,
MaxGroundSpeed = 750,
- MaxAirSpeed = 1,
+ MaxAirSpeed = 750,
AllowZVelocity = false,
ReversePitch = false,
UnlockPitch = false,
VelocityToViewAngles = 0,
RollAmount = 0,
- SprintSpeed = 750,
- RunSpeed = 300,
+ SprintSpeed = 400,
+ RunSpeed = 200,
WalkSpeed = 100,
- DuckSpeed = 25,
+ DuckSpeed = 50,
FinEfficiency = 0,
FinLiftMode = "normal",
@@ -35,6 +42,8 @@ if SERVER then
local str = net.ReadString()
if str == "disable" then
ply.pac_movement = nil
+ ply:GetPhysicsObject():SetMass(default.Mass)
+ ply.scale_mass = 1
else
if default[str] ~= nil then
local val = net.ReadType()
@@ -113,7 +122,9 @@ local function badMovetype(ply)
end
local frictionConvar = GetConVar("sv_friction")
+local lasttime = 0
pac.AddHook("Move", "custom_movement", function(ply, mv)
+ lasttime = SysTime()
local self = ply.pac_movement
if not self then
@@ -145,6 +156,25 @@ pac.AddHook("Move", "custom_movement", function(ply, mv)
ply:SetJumpPower(self.JumpHeight)
+ if SERVER then
+ if allowMass:GetInt() == 1 then
+ ply:GetPhysicsObject():SetMass(math.Clamp(self.Mass, massLowerLimit:GetFloat(), massUpperLimit:GetFloat()))
+ end
+ end
+
+ if (movementConvar:GetInt() == 1 or (movementConvar:GetInt() == -1 and hook.Run("PlayerNoClip", ply, true) == true)) and massDamageScale:GetInt() == 1 then
+ ply.scale_mass = 85/math.Clamp(self.Mass, math.max(massLowerLimit:GetFloat(), 0.01), massUpperLimit:GetFloat())
+ else
+ ply.scale_mass = 1
+ end
+
+ pac.AddHook("EntityTakeDamage", "PAC3MassDamageScale", function(target, dmginfo)
+ if (target:IsPlayer() and dmginfo:IsDamageType(DMG_CRUSH or DMG_VEHICLE)) then
+ dmginfo:ScaleDamage(target.scale_mass or 1)
+ end
+ end)
+
+
if self.Noclip then
ply:SetMoveType(MOVETYPE_NONE)
else
@@ -173,7 +203,11 @@ pac.AddHook("Move", "custom_movement", function(ply, mv)
speed = self.DuckSpeed
end
--- speed = speed * FrameTime()
+ if not on_ground and not self.AllowZVelocity then
+ speed = speed * self.StrafingStrengthMultiplier
+ end
+
+ --speed = speed * FrameTime()
local ang = mv:GetAngles()
local vel = Vector()
@@ -194,6 +228,7 @@ pac.AddHook("Move", "custom_movement", function(ply, mv)
vel = vel - ang:Right()
end
+
vel = vel:GetNormalized() * speed
if self.AllowZVelocity then
@@ -208,15 +243,19 @@ pac.AddHook("Move", "custom_movement", function(ply, mv)
vel.z = 0
end
- local speed = vel
+ local speed = vel --That makes speed the driver (added velocity)
+ if not on_ground and not self.AllowZVelocity then
+ speed = speed * self.StrafingStrengthMultiplier
+ end
local vel = mv:GetVelocity()
+ --@note ground friction
if on_ground and not self.Noclip and self.StickToGround then -- work against ground friction
local sv_friction = frictionConvar:GetInt()
-
+ --ice and glass go too fast? what do?
if sv_friction > 0 then
- sv_friction = 1 - (sv_friction * 15) / 1000
+ sv_friction = 1 - (sv_friction * 15) / 1000 --default is 8, and the formula ends up being equivalent to 0.12 groundfriction variable multiplying vel by 0.88
vel = vel / sv_friction
end
end
@@ -226,14 +265,64 @@ pac.AddHook("Move", "custom_movement", function(ply, mv)
-- todo: don't allow adding more velocity to existing velocity if it exceeds
-- but allow decreasing
if not on_ground then
- local friction = self.AirFriction
- friction = -(friction) + 1
- vel = vel * friction
+ if ply:WaterLevel() >= 2 then
+ local ground_speed = self.RunSpeed
- vel = vel + self.Gravity * 0.015
- speed = speed:GetNormalized() * math.Clamp(speed:Length(), 0, self.MaxAirSpeed)
- vel = vel + (speed * FrameTime()*(66.666*(-friction+1)))
+ if mv:KeyDown(IN_SPEED) then
+ ground_speed = self.SprintSpeed
+ end
+
+ if mv:KeyDown(IN_WALK) then
+ ground_speed = self.WalkSpeed
+ end
+
+ if mv:KeyDown(IN_DUCK) then
+ ground_speed = self.DuckSpeed
+ end
+ if self.MaxGroundSpeed == 0 then self.MaxGroundSpeed = 400 end
+ if self.MaxAirSpeed == 0 then self.MaxAirSpeed = 400 end
+ local water_speed = math.min(ground_speed, self.MaxAirSpeed, self.MaxGroundSpeed)
+ --print("water speed " .. water_speed)
+
+ ang = ply:EyeAngles()
+ local vel2 = Vector()
+
+ if mv:KeyDown(IN_FORWARD) then
+ vel2 = water_speed*ang:Forward()
+ elseif mv:KeyDown(IN_BACK) then
+ vel2 = -water_speed*ang:Forward()
+ end
+
+ if mv:KeyDown(IN_MOVERIGHT) then
+ vel2 = vel2 + ang:Right()
+ elseif mv:KeyDown(IN_MOVELEFT) then
+ vel2 = vel2 - ang:Right()
+ end
+
+ vel = vel + vel2 * math.min(FrameTime(),0.3) * 2
+
+ else
+ local friction = self.AirFriction
+ local friction_mult = -(friction) + 1
+
+ local hfric = friction * self.HorizontalAirFrictionMultiplier
+ local hfric_mult = -(hfric) + 1
+
+ vel.x = vel.x * hfric_mult
+ vel.y = vel.y * hfric_mult
+ vel.z = vel.z * friction_mult
+ vel = vel + self.Gravity * 0.015
+
+ speed = speed:GetNormalized() * math.Clamp(speed:Length(), 0, self.MaxAirSpeed) --base driver speed but not beyond max?
+ --why should the base driver speed depend on friction?
+
+ --reminder: vel is the existing speed, speed is the driver (added velocity)
+ --vel = vel + (speed * FrameTime()*(66.666*friction))
+ vel.x = vel.x + (speed.x * math.min(FrameTime(),0.3)*(66.666*hfric))
+ vel.y = vel.y + (speed.y * math.min(FrameTime(),0.3)*(66.666*hfric))
+ vel.z = vel.z + (speed.z * math.min(FrameTime(),0.3)*(66.666*friction))
+ end
else
local friction = self.GroundFriction
friction = -(friction) + 1
@@ -241,7 +330,24 @@ pac.AddHook("Move", "custom_movement", function(ply, mv)
vel = vel * friction
speed = speed:GetNormalized() * math.min(speed:Length(), self.MaxGroundSpeed)
- vel = vel + (speed * FrameTime()*(75.77*(-friction+1)))
+
+ local trace = {
+ start = mv:GetOrigin(),
+ endpos = mv:GetOrigin() + Vector(0, 0, -20),
+ mask = MASK_SOLID_BRUSHONLY
+ }
+ local trc = util.TraceLine(trace)
+ local special_surf_fric = 1
+ --print(trc.MatType)
+ if trc.MatType == MAT_GLASS then
+ special_surf_fric = 0.6
+ elseif trc.MatType == MAT_SNOW then
+ special_surf_fric = 0.4
+ end
+
+ --vel = vel + (special_surf_fric * speed * FrameTime()*(75.77*(-friction+1)))
+ vel = vel + (special_surf_fric * speed * math.min(FrameTime(),0.3)*(75.77*(-friction+1)))
+
vel = vel + self.Gravity * 0.015
end
diff --git a/lua/pac3/editor/client/animation_timeline.lua b/lua/pac3/editor/client/animation_timeline.lua
index 3ef70db67..687feabb7 100644
--- a/lua/pac3/editor/client/animation_timeline.lua
+++ b/lua/pac3/editor/client/animation_timeline.lua
@@ -63,7 +63,7 @@ local function check_tpose()
end
end
-timeline.interpolation = "cosine"
+timeline.interpolation = "linear"
function timeline.SetInterpolation(str)
timeline.interpolation = str
@@ -115,6 +115,14 @@ function timeline.UpdateFrameData()
timeline.dummy_bone:SetAngles(Angle(data.RR, data.RU, data.RF))
end
+function timeline.Reindex()
+ timeline.frame:Clear()
+ for i, v in ipairs(timeline.data.FrameData) do
+ local keyframe = timeline.frame:AddKeyFrame(true)
+ keyframe:SetFrameData(i, v)
+ end
+end
+
function timeline.EditBone()
pace.Call("PartSelected", timeline.dummy_bone)
local boneData = pac.GetModelBones(timeline.entity)
@@ -174,7 +182,6 @@ function timeline.Load(data)
timeline.frame:Clear()
timeline.SelectKeyframe(timeline.frame:AddKeyFrame())
- timeline.Save()
end
timeline.UpdateFrameData()
@@ -249,8 +256,8 @@ function timeline.Open(part)
timeline.entity = part:GetOwner()
timeline.frame = vgui.Create("pac3_timeline")
- timeline.frame:SetSize(ScrW()-pace.Editor:GetWide(),93)
- timeline.frame:SetPos(pace.Editor:GetWide(),ScrH()-timeline.frame:GetTall())
+ timeline.frame:SetSize(ScrW()-pace.Editor:GetWide(), 93)
+ timeline.frame:SetPos(pace.Editor:GetWide(), ScrH()-timeline.frame:GetTall())
timeline.frame:SetTitle("")
timeline.frame:ShowCloseButton(false)
@@ -343,36 +350,36 @@ do
local TIMELINE = {}
function TIMELINE:Init()
- self:DockMargin(0,0,0,0)
- self:DockPadding(0,35,0,0)
+ self:DockMargin(0, 0, 0, 0)
+ self:DockPadding(0, 30, 0, 0)
do -- time display info
local time = self:Add("DPanel")
local test = L"frame" .. ": 10.888"
surface.SetFont(pace.CurrentFont)
- local w,h = surface.GetTextSize(test)
+ local w, h = surface.GetTextSize(test)
time:SetWide(w)
time:SetTall(h*2 + 2)
- time:SetPos(0,1)
- time.Paint = function(s, w,h)
+ time:SetPos(0, 1)
+ time.Paint = function(s, w, h)
self:GetSkin().tex.Tab_Control( 0, 0, w, h )
self:GetSkin().tex.CategoryList.Header( 0, 0, w, h )
if not timeline.animation_part then return end
- local w,h = draw.TextShadow({
+ local w, h = draw.TextShadow({
text = L"frame" .. ": " .. (animations.GetEntityAnimationFrame(timeline.entity, timeline.animation_part:GetAnimID()) or 0),
font = pace.CurrentFont,
- pos = {2, 0},
+ pos = {5, 0},
color = self:GetSkin().Colours.Category.Header
}, 1, 100)
draw.TextShadow({
text = L"time" .. ": " .. math.Round(timeline.GetCycle() * animations.GetAnimationDuration(timeline.entity, timeline.animation_part:GetAnimID()), 3),
font = pace.CurrentFont,
- pos = {2, h},
+ pos = {5, h},
color = self:GetSkin().Colours.Category.Header
}, 1, 100)
end
@@ -393,18 +400,18 @@ do
local spacing = (size - 24)/2
local play = controls:Add("DButton")
- play:SetSize(size,size)
+ play:SetSize(size, size)
play:SetText("")
play.DoClick = function() self:Toggle() end
play:Dock(LEFT)
local stop = controls:Add("DButton")
- stop:SetSize(size,size)
+ stop:SetSize(size, size)
stop:SetText("")
stop.DoClick = function() self:Stop() end
stop:Dock(LEFT)
- function play.PaintOver(_,w,h)
+ function play.PaintOver(_, w, h)
surface.SetDrawColor(self:GetSkin().Colours.Button.Normal)
draw.NoTexture()
if self:IsPlaying() then
@@ -419,9 +426,9 @@ do
end
end
- function stop:PaintOver(w,h)
+ function stop:PaintOver(w, h)
surface.SetDrawColor(self:GetSkin().Colours.Button.Normal)
- surface.DrawRect(spacing,spacing,24,24)
+ surface.DrawRect(spacing, spacing, 24, 24)
end
end
do -- save/load
@@ -491,7 +498,7 @@ do
menu:PerformLayout()
- local x, y = bottom:LocalToScreen(0,0)
+ local x, y = bottom:LocalToScreen(0, 0)
x = x + bottom:GetWide()
menu:SetPos(x - menu:GetWide(), y - menu:GetTall())
end
@@ -500,10 +507,10 @@ do
end
do -- keyframes
- local pnl = vgui.Create("pac_scrollpanel_horizontal",self)
+ local pnl = vgui.Create("pac_scrollpanel_horizontal", self)
pnl:Dock(FILL)
- pnl:GetCanvas().Paint = function(_,w,h)
+ pnl:GetCanvas().Paint = function(_, w, h)
derma.SkinHook( "Paint", "ListBox", self, w, h )
end
@@ -528,7 +535,7 @@ do
if self.moving then return end
local x = 0
- for k,v in ipairs(pnl:GetCanvas():GetChildren()) do
+ for k, v in ipairs(pnl:GetCanvas():GetChildren()) do
v:SetWide(math.max(1/v:GetData().FrameRate * secondDistance, 4))
v:SetTall(h)
v:SetPos(x, 0)
@@ -540,7 +547,7 @@ do
end
do -- timeline
- local pnl = vgui.Create("DPanel",self)
+ local pnl = vgui.Create("DPanel", self)
surface.SetFont(pace.CurrentFont)
local _, h = surface.GetTextSize("|")
@@ -571,20 +578,19 @@ do
local start = Material("icon16/control_play_blue.png")
local restart = Material("icon16/control_repeat_blue.png")
local estyle = Material("icon16/arrow_branch.png")
- pnl.Paint = function(s,w,h)
+ pnl.Paint = function(s, w, h)
local offset = -self.keyframe_scroll:GetCanvas():GetPos()
- local esoffset = self.keyframe_scroll:GetCanvas():GetPos()
self:GetSkin().tex.Tab_Control( 0, 0, w, h )
self:GetSkin().tex.CategoryList.Header( 0, 0, w, h )
local previousSecond = offset-(offset%secondDistance)
- for i=previousSecond,previousSecond+s:GetWide(),secondDistance/2 do
+ for i = previousSecond, previousSecond+s:GetWide(), secondDistance/2 do
if i-offset > 0 and i-offset < ScrW() then
local sec = i/secondDistance
local x = i-offset
- surface.SetDrawColor(0,0,0,100)
+ surface.SetDrawColor(0, 0, 0, 100)
surface.DrawLine(x+1, 1+1, x+1, pnl:GetTall() - 3+1)
surface.SetDrawColor(self:GetSkin().Colours.Category.Header)
@@ -592,7 +598,7 @@ do
surface.SetTextPos(x+2+1, 1+1)
surface.SetFont(pace.CurrentFont)
- surface.SetTextColor(0,0,0,100)
+ surface.SetTextColor(0, 0, 0, 100)
surface.DrawText(sec)
surface.SetTextPos(x+2, 1)
@@ -602,10 +608,10 @@ do
end
end
- for i=previousSecond,previousSecond+s:GetWide(),secondDistance/8 do
+ for i = previousSecond, previousSecond+s:GetWide(), secondDistance/8 do
if i-offset > 0 and i-offset < ScrW() then
local x = i-offset
- surface.SetDrawColor(0,0,0,100)
+ surface.SetDrawColor(0, 0, 0, 100)
surface.DrawLine(x+1, 1+1, x+1, pnl:GetTall()/2+1)
surface.SetDrawColor(self:GetSkin().Colours.Category.Header)
@@ -623,24 +629,28 @@ do
local esmat = v.estyle and estyle or false
if mat then
- local x = v:GetPos()
- surface.SetDrawColor(255,255,255,200)
+ local x = v:GetPos() - offset
+ if x > s:GetWide() - 10 then continue end
+ surface.SetDrawColor(255, 255, 255, 200)
surface.DrawLine(x, -mat:Height()/2 - 5, x, h)
- surface.SetDrawColor(255,255,255,255)
+ surface.SetDrawColor(255, 255, 255, 255)
surface.SetMaterial(mat)
- surface.DrawTexturedRect(1+x,mat:Height() - 5,mat:Width(), mat:Height())
+ surface.DrawTexturedRect(1+x, mat:Height() - 5, mat:Width(), mat:Height())
end
if esmat then
local ps = v:GetSize()
- local x = v:GetPos() + (ps * 0.5)
- surface.SetDrawColor(255,255,255,255)
+ local x = v:GetPos() - offset + (ps * 0.5)
+ if x > s:GetWide() - 10 then continue end
+ surface.SetDrawColor(255, 255, 255, 255)
surface.SetMaterial(esmat)
- surface.DrawTexturedRect(1+x - (esmat:Width() * 0.5), esmat:Height(),esmat:Width(), esmat:Height())
+ surface.DrawTexturedRect(1+x - (esmat:Width() * 0.5), esmat:Height(), esmat:Width(), esmat:Height())
if ps >= 65 then
- draw.SimpleText( v.estyle, "Default", x, esmat:Height() * 2, color_white, TEXT_ALIGN_CENTER, TEXT_ALIGN_TOP )
+ draw.SimpleText( v.estyle, pace.CurrentFont, x, esmat:Height() * 2, self:GetSkin().Colours.Label.Dark, TEXT_ALIGN_CENTER, TEXT_ALIGN_TOP )
+ else
+ v:SetTooltip(v.estyle)
end
end
end
@@ -650,17 +660,17 @@ do
local x = timeline.GetCycle() * self.keyframe_scroll:GetCanvas():GetWide()
x = x - offset
- surface.SetDrawColor(255,0,0,200)
+ surface.SetDrawColor(255, 0, 0, 200)
surface.DrawLine(x, 0, x, h)
- surface.SetDrawColor(255,0,0,255)
+ surface.SetDrawColor(255, 0, 0, 255)
surface.SetMaterial(scrub)
- surface.DrawTexturedRect(1 + x - scrub:Width()/2,-11,scrub:Width(), scrub:Height())
+ surface.DrawTexturedRect(1 + x - scrub:Width()/2, -11, scrub:Width(), scrub:Height())
end
end
end
- function TIMELINE:Paint(w,h)
+ function TIMELINE:Paint(w, h)
self:GetSkin().tex.Tab_Control(0, 35, w, h-35)
end
@@ -668,11 +678,11 @@ do
DFrame.Think(self)
if pace.Editor:GetPos() + pace.Editor:GetWide() / 2 < ScrW() / 2 then
- self:SetSize(ScrW()-(pace.Editor.x+pace.Editor:GetWide()),93)
- self:SetPos(pace.Editor.x+pace.Editor:GetWide(),ScrH()-self:GetTall())
+ self:SetSize(ScrW()-(pace.Editor.x+pace.Editor:GetWide()), 93)
+ self:SetPos(pace.Editor.x+pace.Editor:GetWide(), ScrH()-self:GetTall())
else
- self:SetSize(ScrW()-(ScrW()-pace.Editor.x),93)
- self:SetPos(0,ScrH()-self:GetTall())
+ self:SetSize(ScrW()-(ScrW()-pace.Editor.x), 93)
+ self:SetPos(0, ScrH()-self:GetTall())
end
if input.IsKeyDown(KEY_SPACE) then
@@ -729,7 +739,7 @@ do
end
function TIMELINE:Clear()
- for i,v in pairs(self.keyframe_scroll:GetCanvas():GetChildren()) do
+ for i, v in pairs(self.keyframe_scroll:GetCanvas():GetChildren()) do
v:Remove()
end
self.add_keyframe_button:SetDisabled(false)
@@ -754,7 +764,7 @@ do
local restartFrame = timeline.data.RestartFrame
if not restartFrame then return 0 end --no restart pos? start at the start
- for i,v in ipairs(timeline.data.FrameData) do
+ for i, v in ipairs(timeline.data.FrameData) do
if i == restartFrame then return timeInSeconds end
timeInSeconds = timeInSeconds+(1/(v.FrameRate or 1))
end
@@ -768,7 +778,7 @@ do
local startFrame = timeline.data.StartFrame
if not startFrame then return 0 end --no restart pos? start at the start
- for i,v in ipairs(timeline.data.FrameData) do
+ for i, v in ipairs(timeline.data.FrameData) do
if i == startFrame then return timeInSeconds end
timeInSeconds = timeInSeconds+(1/(v.FrameRate or 1))
end
@@ -793,7 +803,7 @@ do
return keyframe
end
- vgui.Register("pac3_timeline",TIMELINE,"DFrame")
+ vgui.Register("pac3_timeline", TIMELINE, "DFrame")
end
do
@@ -830,7 +840,8 @@ do
function KEYFRAME:GetData()
return self.DataTable
end
- function KEYFRAME:SetFrameData(index,tbl)
+
+ function KEYFRAME:SetFrameData(index, tbl)
self.DataTable = tbl
self.AnimationKeyIndex = index
self:GetParent():GetParent():InvalidateLayout() --rebuild the timeline
@@ -849,19 +860,19 @@ do
return self.AnimationKeyIndex
end
- function KEYFRAME:Paint(w,h)
+ function KEYFRAME:Paint(w, h)
self.AltLine = self.Alternate
derma.SkinHook( "Paint", "CategoryButton", self, w, h )
if timeline.selected_keyframe == self then
local c = self:GetSkin().Colours.Category.Line.Button_Selected
- surface.SetDrawColor(c.r,c.g,c.b,250)
+ surface.SetDrawColor(c.r, c.g, c.b, 250)
end
- surface.DrawRect(0,0,w,h)
+ surface.DrawRect(0, 0, w, h)
- surface.SetDrawColor(0,0,0,75)
- surface.DrawOutlinedRect(0,0,w,h)
+ surface.SetDrawColor(0, 0, 0, 75)
+ surface.DrawOutlinedRect(0, 0, w, h)
end
function KEYFRAME:Think()
@@ -897,14 +908,14 @@ do
return (a:GetPos() + a:GetWide() / 2) < (b:GetPos() + b:GetWide() / 2)
end)
- for i,v in ipairs(panels) do
+ for i, v in ipairs(panels) do
v:SetParent(timeline.frame.keyframe_scroll)
v.Alternate = #timeline.frame.keyframe_scroll:GetCanvas():GetChildren()%2 == 1
frames[i] = timeline.data.FrameData[v:GetAnimationIndex()]
end
- for i,v in ipairs(frames) do
+ for i, v in ipairs(frames) do
timeline.data.FrameData[i] = v
panels[i].AnimationKeyIndex = i
end
@@ -940,8 +951,9 @@ do
timeline.frame:Toggle(false)
timeline.SelectKeyframe(self)
elseif mc == MOUSE_RIGHT then
+ timeline.SelectKeyframe(self)
local menu = DermaMenu()
- menu:AddOption(L"set length",function()
+ menu:AddOption(L"set length", function()
Derma_StringRequest(L"question",
L"how long should this frame be in seconds?",
tostring(self:GetWide()/secondDistance),
@@ -951,7 +963,7 @@ do
L"cancel" )
end):SetImage("icon16/time.png")
- menu:AddOption(L"multiply length",function()
+ menu:AddOption(L"multiply length", function()
Derma_StringRequest(L"question",
L"multiply "..self:GetAnimationIndex().."'s length",
"1.0",
@@ -962,36 +974,36 @@ do
end):SetImage("icon16/time_add.png")
if not self:GetRestart() then
- menu:AddOption(L"set restart",function()
- for _,v in pairs(timeline.frame.keyframe_scroll:GetCanvas():GetChildren()) do
+ menu:AddOption(L"set restart", function()
+ for _, v in pairs(timeline.frame.keyframe_scroll:GetCanvas():GetChildren()) do
v:SetRestart(false)
end
self:SetRestart(true)
timeline.data.RestartFrame = self:GetAnimationIndex()
end):SetImage("icon16/control_repeat_blue.png")
else
- menu:AddOption(L"unset restart",function()
+ menu:AddOption(L"unset restart", function()
self:SetRestart(false)
timeline.data.StartFrame = nil
end):SetImage("icon16/control_repeat.png")
end
if not self:GetStart() then
- menu:AddOption(L"set start",function()
- for _,v in pairs(timeline.frame.keyframe_scroll:GetCanvas():GetChildren()) do
+ menu:AddOption(L"set start", function()
+ for _, v in pairs(timeline.frame.keyframe_scroll:GetCanvas():GetChildren()) do
v:SetStart(false)
end
self:SetStart(true)
timeline.data.StartFrame = self:GetAnimationIndex()
end):SetImage("icon16/control_play_blue.png")
else
- menu:AddOption(L"unset start",function()
+ menu:AddOption(L"unset start", function()
self:SetStart(false)
timeline.data.StartFrame = nil
end):SetImage("icon16/control_play.png")
end
- menu:AddOption(L"reverse",function()
+ menu:AddOption(L"reverse", function()
local frame = timeline.data.FrameData[self:GetAnimationIndex() - 1]
if not frame then
frame = timeline.data.FrameData[#timeline.data.FrameData]
@@ -1009,32 +1021,29 @@ do
timeline.UpdateFrameData()
end):SetImage("icon16/control_rewind_blue.png")
- menu:AddOption(L"duplicate to end", function()
- local keyframe = timeline.frame:AddKeyFrame()
+ local function duplicateTo(index)
+ local data = self:GetData();
+ table.insert(timeline.data.FrameData, index, table.Copy(data))
+ timeline.Reindex()
- local tbl = self:GetData().BoneInfo
- for i, v in pairs(tbl) do
- local data = keyframe:GetData()
- data.BoneInfo[i] = table.Copy(self:GetData().BoneInfo[i] or {})
- data.BoneInfo[i].MU = v.MU
- data.BoneInfo[i].MR = v.MR
- data.BoneInfo[i].MF = v.MF
- data.BoneInfo[i].RU = v.RU
- data.BoneInfo[i].RR = v.RR
- data.BoneInfo[i].RF = v.RF
- end
- keyframe:SetLength(1/(self:GetData().FrameRate))
- timeline.SelectKeyframe(keyframe)
- end):SetImage("icon16/application_double.png")
+ timer.Simple(0, function()
+ timeline.SelectKeyframe(timeline.frame.keyframe_scroll:GetCanvas():GetChildren()[index])
+ end)
+ end
- menu:AddOption(L"remove",function()
+ local sub, opt = menu:AddSubMenu(L"duplicate", function() duplicateTo(self:GetAnimationIndex()) end)
+ sub:AddOption(L"start", function() duplicateTo(1) end):SetImage("icon16/resultset_first.png")
+ sub:AddOption(L"end", function() duplicateTo(#timeline.data.FrameData + 1) end):SetImage("icon16/resultset_last.png")
+ opt:SetIcon("icon16/page_copy.png")
+
+ menu:AddOption(L"remove", function()
local frameNum = self:GetAnimationIndex()
if frameNum == 1 and not timeline.data.FrameData[2] then return end
table.remove(timeline.data.FrameData, frameNum)
local remove_i
- for i,v in pairs(timeline.frame.keyframe_scroll:GetCanvas():GetChildren()) do
+ for i, v in pairs(timeline.frame.keyframe_scroll:GetCanvas():GetChildren()) do
if v == self then
remove_i = i
elseif v:GetAnimationIndex() > frameNum then
@@ -1048,14 +1057,14 @@ do
timeline.frame.keyframe_scroll:InvalidateLayout()
self:Remove()
- -- * even if it was removed from the table it still exists for some reason
+
local count = #timeline.frame.keyframe_scroll:GetCanvas():GetChildren()
- local offset = frameNum == count and count - 1 or count
+ local offset = remove_i >= count and count - 1 or remove_i + 1
timeline.SelectKeyframe(timeline.frame.keyframe_scroll:GetCanvas():GetChildren()[offset])
- end):SetImage("icon16/application_delete.png")
+ end):SetImage("icon16/page_delete.png")
menu:AddOption(L"set easing style", function()
- if timeline.data.Interpolation != "linear" then
+ if timeline.data.Interpolation ~= "linear" then
local frame = vgui.Create("DFrame")
frame:SetSize(300, 100)
frame:Center()
@@ -1072,8 +1081,6 @@ do
return
end
- local frameNum = self:GetAnimationIndex()
-
local frame = vgui.Create( "DFrame" )
frame:SetSize( 200, 100 )
frame:Center()
@@ -1121,12 +1128,14 @@ do
if not style then return end
self:GetData().EaseStyle = style
self.estyle = style
+ timeline.Save()
end
function KEYFRAME:RemoveEaseStyle()
self:GetData().EaseStyle = nil
self.estyle = nil
+ timeline.Save()
end
- vgui.Register("pac3_timeline_keyframe",KEYFRAME,"DPanel")
+ vgui.Register("pac3_timeline_keyframe", KEYFRAME, "DPanel")
end
\ No newline at end of file
diff --git a/lua/pac3/editor/client/asset_browser.lua b/lua/pac3/editor/client/asset_browser.lua
index c4ea9969c..fd8910a1e 100644
--- a/lua/pac3/editor/client/asset_browser.lua
+++ b/lua/pac3/editor/client/asset_browser.lua
@@ -1,6 +1,87 @@
-- based on starfall
CreateClientConVar("pac_asset_browser_close_on_select", "1")
CreateClientConVar("pac_asset_browser_remember_layout", "1")
+CreateClientConVar("pac_asset_browser_extra_options", "1")
+CreateClientConVar("pac_favorites_try_to_get_asset_series", "1")
+CreateClientConVar("pac_favorites_try_to_build_asset_series", "0")
+
+local function rebuild_bookmarks()
+ pace.bookmarked_ressources = pace.bookmarked_ressources or {}
+
+ --here's some default favorites
+ if not pace.bookmarked_ressources["models"] or table.IsEmpty(pace.bookmarked_ressources["models"]) then
+ pace.bookmarked_ressources["models"] = {
+ "models/pac/default.mdl",
+ "models/pac/plane.mdl",
+ "models/pac/circle.mdl",
+ "models/hunter/blocks/cube025x025x025.mdl",
+ "models/editor/axis_helper.mdl",
+ "models/editor/axis_helper_thick.mdl"
+ }
+ end
+
+ if not pace.bookmarked_ressources["sound"] or table.IsEmpty(pace.bookmarked_ressources["sound"]) then
+ pace.bookmarked_ressources["sound"] = {
+ "music/hl1_song11.mp3",
+ "npc/combine_gunship/dropship_engine_near_loop1.wav",
+ "ambient/alarms/warningbell1.wav",
+ "phx/epicmetal_hard7.wav",
+ "phx/explode02.wav"
+ }
+ end
+
+ if not pace.bookmarked_ressources["materials"] or table.IsEmpty(pace.bookmarked_ressources["materials"]) then
+ pace.bookmarked_ressources["materials"] = {
+ "models/debug/debugwhite",
+ "vgui/null",
+ "debug/env_cubemap_model",
+ "models/wireframe",
+ "cable/physbeam",
+ "cable/cable2",
+ "effects/tool_tracer",
+ "effects/flashlight/logo",
+ "particles/flamelet[1,5]",
+ "sprites/key_[0,9]",
+ "vgui/spawnmenu/generating",
+ "vgui/spawnmenu/hover"
+ }
+ end
+end
+
+local function encode_table_to_file(str)
+ local data = {}
+ if not file.Exists("pac3_config", "DATA") then
+ file.CreateDir("pac3_config")
+
+ end
+
+
+ if str == "pac_editor_shortcuts" then
+ data = pace.PACActionShortcut
+ file.Write("pac3_config/" .. str..".txt", util.TableToKeyValues(data))
+ elseif str == "pac_editor_partmenu_layouts" then
+ data = pace.operations_order
+ file.Write("pac3_config/" .. str..".txt", util.TableToJSON(data))
+ elseif str == "pac_part_categories" then
+ data = pace.partgroups
+ file.Write("pac3_config/" .. str..".txt", util.TableToKeyValues(data))
+ elseif str == "bookmarked_ressources" then
+ rebuild_bookmarks()
+ for category, tbl in pairs(pace.bookmarked_ressources) do
+ data = tbl
+ str = category
+ file.Write("pac3_config/bookmarked_" .. str..".txt", util.TableToKeyValues(data))
+ end
+
+ end
+
+end
+
+function pace.SaveRessourceBookmarks()
+ encode_table_to_file("bookmarked_ressources")
+end
+
+
local function table_tolist(tbl, sort)
local list = {}
@@ -51,6 +132,7 @@ end
local L = pace.LanguageString
+--icon is the item's panel, path is the item's full file path, on_menu is a function that can extend the function
local function install_click(icon, path, pattern, on_menu, pathid)
local old = icon.OnMouseReleased
icon.OnMouseReleased = function(_, code)
@@ -70,6 +152,42 @@ local function install_click(icon, path, pattern, on_menu, pathid)
end
SetClipboardText(path)
end)
+ local resource_type = ""
+ if string.match(path, "^materials/(.+)%.vmt$") or string.match(path, "^materials/(.+%.png)$") then resource_type = "materials"
+ elseif string.match(path, "^models/") then resource_type = "models" end
+
+ if not pace.bookmarked_ressources then
+ pace.SaveRessourceBookmarks()
+ elseif not pace.bookmarked_ressources[resource_type] then
+ pace.SaveRessourceBookmarks()
+ end
+ if GetConVar("pac_asset_browser_extra_options"):GetBool() and pace.bookmarked_ressources[resource_type] then
+ if GetConVar("pac_favorites_try_to_get_asset_series"):GetBool() then
+ if not table.HasValue(pace.bookmarked_ressources[resource_type], path) then
+ menu:AddOption(L"add series to favorites", function()
+ table.insert(pace.bookmarked_ressources[resource_type], path)
+ pace.SaveRessourceBookmarks()
+ end):SetImage("icon16/star.png")
+ else
+ menu:AddOption(L"remove series from favorites", function()
+ table.remove(pace.bookmarked_ressources[resource_type], table.KeyFromValue( pace.bookmarked_ressources[resource_type], path ))
+ pace.SaveRessourceBookmarks()
+ end):SetImage("icon16/cross.png")
+ end
+ end
+ if not table.HasValue(pace.bookmarked_ressources[resource_type], path) then
+ menu:AddOption(L"add to favorites", function()
+ table.insert(pace.bookmarked_ressources[resource_type], path)
+ pace.SaveRessourceBookmarks()
+ end):SetImage("icon16/star.png")
+ else
+ menu:AddOption(L"remove from favorites", function()
+ table.remove(pace.bookmarked_ressources[resource_type], table.KeyFromValue( pace.bookmarked_ressources[resource_type], path ))
+ pace.SaveRessourceBookmarks()
+ end):SetImage("icon16/cross.png")
+ end
+ end
+
if on_menu then on_menu(menu) end
menu:Open()
end
@@ -78,6 +196,32 @@ local function install_click(icon, path, pattern, on_menu, pathid)
end
end
+local function install_right_click_for_favorite_folder(icon, path, pathid, resource_type)
+ resource_type = resource_type or pace.model_browser_browse_types_tbl[1] or "models"
+ icon.DoRightClick = function()
+ local menu = DermaMenu()
+ if not table.HasValue(pace.bookmarked_ressources[resource_type], "folder:" .. path) then
+ menu:AddOption(L"add folder to favorites : " .. path, function()
+ table.insert(pace.bookmarked_ressources[resource_type], "folder:" .. path)
+ pace.SaveRessourceBookmarks()
+ end):SetImage("icon16/star.png")
+ else
+ menu:AddOption(L"remove folder from favorites : " .. path, function()
+ table.remove(pace.bookmarked_ressources[resource_type], table.KeyFromValue( pace.bookmarked_ressources[resource_type], "folder:" .. path ))
+ pace.SaveRessourceBookmarks()
+ end):SetImage("icon16/cross.png")
+ end
+ menu:Open()
+ end
+ timer.Simple(1, function()
+ if not icon.GetChildNodes then return end
+ for i,child in ipairs(icon:GetChildNodes()) do
+ install_right_click_for_favorite_folder(child, child:GetFolder(), child:GetPathID(), resource_type)
+ end
+ end)
+
+end
+
local function get_unlit_mat(path)
if path:find("%.png$") then
return Material(path:match("materials/(.+)"))
@@ -93,6 +237,10 @@ local function get_unlit_mat(path)
return CreateMaterial(path .. "_pac_asset_browser", "UnlitGeneric", {["$basetexture"] = path:match("materials/(.+)%.vtf")})
end
+function pace.get_unlit_mat(path)
+ return get_unlit_mat(path)
+end
+
local next_generate_icon = 0
local max_generating = 5
@@ -215,25 +363,25 @@ local function create_material_icon(path, grid_panel)
--[[
- local old = icon.OnCursorEntered
- function icon:OnCursorEntered(...)
- if pace.current_part:IsValid() and pace.current_part.Materialm then
- pace.asset_browser_old_mat = pace.asset_browser_old_mat or pace.current_part.Materialm
- pace.current_part.Materialm = mat
- end
+ local old = icon.OnCursorEntered
+ function icon:OnCursorEntered(...)
+ if pace.current_part:IsValid() and pace.current_part.Materialm then
+ pace.asset_browser_old_mat = pace.asset_browser_old_mat or pace.current_part.Materialm
+ pace.current_part.Materialm = mat
+ end
- old(self, ...)
- end
+ old(self, ...)
+ end
- local old = icon.OnCursorExited
- function icon:OnCursorExited(...)
- if pace.current_part:IsValid() and pace.current_part.Materialm then
- pace.current_part.Materialm = pace.asset_browser_old_mat
+ local old = icon.OnCursorExited
+ function icon:OnCursorExited(...)
+ if pace.current_part:IsValid() and pace.current_part.Materialm then
+ pace.current_part.Materialm = pace.asset_browser_old_mat
+ end
+ old(self, ...)
end
- old(self, ...)
- end
-]]
+ ]]
local unlit_mat = get_unlit_mat(path)
@@ -343,6 +491,7 @@ local function create_material_icon(path, grid_panel)
create_text_view(str)
end)
+
end)
grid_panel:Add(icon)
@@ -606,6 +755,7 @@ function pace.AssetBrowser(callback, browse_types_str, part_key)
local frame = vgui.Create("DFrame")
frame.title = L"asset browser" .. " - " .. (browse_types_str:gsub(";", " "))
+
if GetConVar("pac_asset_browser_remember_layout"):GetBool() then
frame:SetCookieName("pac_asset_browser")
end
@@ -681,6 +831,9 @@ function pace.AssetBrowser(callback, browse_types_str, part_key)
options_menu:SetDeleteSelf(false)
options_menu:AddCVar(L"close browser on select", "pac_asset_browser_close_on_select", "1", "0")
options_menu:AddCVar(L"remember layout", "pac_asset_browser_remember_layout", "1", "0")
+ options_menu:AddCVar(L"additional right click options", "pac_asset_browser_extra_options", "1", "0")
+ options_menu:AddCVar(L"try to find asset series for saving favorites", "pac_favorites_try_to_get_asset_series", "1", "0")
+ options_menu:AddCVar(L"try to build asset series in the editor", "pac_favorites_try_to_build_asset_series", "1", "0")
local zoom_controls = vgui.Create("pac_AssetBrowser_ZoomControls", menu_bar)
@@ -772,9 +925,11 @@ function pace.AssetBrowser(callback, browse_types_str, part_key)
sound_list:Dock(FILL)
sound_list:SetMultiSelect(false)
sound_list:SetVisible(false)
-
+ frame.sound = ""
+ frame.lines = {}
local function AddGeneric(self, sound, ...)
local line = self:AddLine(sound, ...)
+ table.insert(frame.lines, line)
local play = vgui.Create("DImageButton", line)
play:SetImage("icon16/control_play.png")
play:SizeToContents()
@@ -813,7 +968,74 @@ function pace.AssetBrowser(callback, browse_types_str, part_key)
if code == MOUSE_RIGHT then
play:Start()
+ local menu = DermaMenu()
+ menu:SetPos(input.GetCursorPos())
+ menu:AddOption(L"copy path", function()
+ SetClipboardText(sound)
+ end)
+ if GetConVar("pac_asset_browser_extra_options"):GetBool() then
+ pace.bookmarked_ressources["sound"] = pace.bookmarked_ressources["sound"] or {}
+ local resource_type = "sound"
+ if GetConVar("pac_favorites_try_to_get_asset_series"):GetBool() then
+ --print(sound)
+ local extension = string.GetExtensionFromFilename(sound)
+ local base_name = string.gsub(sound, "%d+."..extension.."$", "")
+ base_name = string.gsub(base_name, "^sound/", "")
+ --print(resource_type, base_name, extension)
+
+ local series_results = pace.FindAssetSeriesBounds(resource_type, base_name, extension)
+ --PrintTable(series_results)
+ if not series_results.start_index then
+ goto CONTINUE
+ end
+
+ local series_str = base_name .. "[" .. series_results.start_index .. "," .. series_results.end_index .. "]." .. extension
+
+ if not table.HasValue(pace.bookmarked_ressources[resource_type], series_str) then
+
+ menu:AddOption(L"add series to favorites", function()
+ table.insert(pace.bookmarked_ressources[resource_type], series_str)
+ pace.SaveRessourceBookmarks()
+
+ end):SetImage("icon16/star.png")
+ else
+ menu:AddOption(L"remove series from favorites", function()
+ table.remove(pace.bookmarked_ressources[resource_type], table.KeyFromValue( pace.bookmarked_ressources[resource_type], series_str ))
+ pace.SaveRessourceBookmarks()
+
+ end):SetImage("icon16/cross.png")
+ end
+
+ ::CONTINUE::
+ end
+
+ if not table.HasValue(pace.bookmarked_ressources["sound"], sound) then
+ menu:AddOption(L"add to favorites", function()
+ table.insert(pace.bookmarked_ressources["sound"], sound)
+ pace.SaveRessourceBookmarks()
+ end):SetImage("icon16/star.png")
+ else
+ menu:AddOption(L"remove from favorites", function()
+ table.remove(pace.bookmarked_ressources["sound"], table.KeyFromValue( pace.bookmarked_ressources["sound"], sound ))
+ pace.SaveRessourceBookmarks()
+ end):SetImage("icon16/cross.png")
+ end
+ end
+ if not frame.QuickListBuildMode then
+ local pnl = menu:AddOption("Enable Quick list build mode", function() frame.QuickListBuildMode = true frame.sound = "" end) pnl:SetTooltip("Left click will concatenate a new sound to the part's list using semicolon notation. Preview using the play button instead.")
+ else
+ menu:AddOption("Disable Quick list build mode", function()
+ frame.QuickListBuildMode = nil frame.sound = ""
+ for i,v in ipairs(frame.lines) do if v.Columns then v.Columns[1]:SetColor(Color(0,0,0)) end end
+ end)
+ end
+ menu:MakePopup() menu:RequestFocus()
else
+ if frame.QuickListBuildMode then
+ line.Columns[1]:SetColor(Color(0,250,60)) --the file name
+ if frame.sound ~= "" then sound = frame.sound .. ";" .. sound end
+ frame.sound = sound
+ end
pace.model_browser_callback(sound, "GAME")
end
end
@@ -1019,6 +1241,7 @@ function pace.AssetBrowser(callback, browse_types_str, part_key)
do -- mounted
local function addBrowseContent(viewPanel, node, name, icon, path, pathid)
local function on_select(self, node)
+ install_right_click_for_favorite_folder(node, node:GetFolder(), pathid, resource_type)
if viewPanel and viewPanel.currentNode and viewPanel.currentNode == node then return end
node.dir = self.dir
@@ -1134,12 +1357,12 @@ function pace.AssetBrowser(callback, browse_types_str, part_key)
tree:OnNodeSelected(node)
viewPanel.currentNode = node
end
-
+ local oldnode = node
+ oldnode.name = name
node = node:AddNode(name, icon)
node:SetFolder("")
node:SetPathID(pathid)
node.viewPanel = viewPanel
-
for _, dir in ipairs(browse_types) do
local files, folders = file.Find(path .. dir .. "/*", pathid)
if files and (files[1] or folders[1]) then
@@ -1160,6 +1383,8 @@ function pace.AssetBrowser(callback, browse_types_str, part_key)
end
node.OnNodeSelected = on_select
+ install_right_click_for_favorite_folder(node, node:GetFolder(), node:GetPathID(), resource_type)
+
end
local viewPanel = vgui.Create("pac_AssetBrowser_ContentContainer", frame.PropPanel)
diff --git a/lua/pac3/editor/client/fonts.lua b/lua/pac3/editor/client/fonts.lua
index 4a70c106c..e94c0302a 100644
--- a/lua/pac3/editor/client/fonts.lua
+++ b/lua/pac3/editor/client/fonts.lua
@@ -2,7 +2,8 @@ local L = pace.LanguageString
pace.Fonts = {}
-for i = 1, 5 do
+
+for i = 1, 7 do
surface.CreateFont("pac_font_"..i,
{
font = "Arial",
@@ -14,15 +15,39 @@ for i = 1, 5 do
table.insert(pace.Fonts, "pac_font_"..i)
end
-for i = 1, 5 do
- surface.CreateFont("pac_font_bold"..i,
+for i = 8, 32, 4 do
+ surface.CreateFont("pac_font_"..i,
+ {
+ font = "Arial",
+ size = 11 + i,
+ weight = 50,
+ antialias = true,
+ })
+
+ table.insert(pace.Fonts, "pac_font_"..i)
+end
+
+for i = 1, 7 do
+ surface.CreateFont("pac_font_bold_"..i,
{
font = "Arial",
size = 11 + i,
weight = 800,
antialias = true,
})
- table.insert(pace.Fonts, "pac_font_bold"..i)
+ table.insert(pace.Fonts, "pac_font_bold_"..i)
+end
+
+for i = 8, 32, 4 do
+ surface.CreateFont("pac_font_bold_"..i,
+ {
+ font = "Arial",
+ size = 11 + i,
+ weight = 50,
+ antialias = true,
+ })
+
+ table.insert(pace.Fonts, "pac_font_bold_"..i)
end
table.insert(pace.Fonts, "DermaDefault")
diff --git a/lua/pac3/editor/client/icons.lua b/lua/pac3/editor/client/icons.lua
index 85f5fd21b..a564b121c 100644
--- a/lua/pac3/editor/client/icons.lua
+++ b/lua/pac3/editor/client/icons.lua
@@ -31,6 +31,7 @@ pace.MiscIcons = {
pace.GroupsIcons = {
effects = 'icon16/wand.png',
model = 'icon16/shape_square.png',
+ combat = 'icon16/joystick.png',
entity = 'icon16/brick.png',
modifiers = 'icon16/disconnect.png',
advanced = 'icon16/page_white_gear.png',
diff --git a/lua/pac3/editor/client/init.lua b/lua/pac3/editor/client/init.lua
index 933d58a9b..7de793b69 100644
--- a/lua/pac3/editor/client/init.lua
+++ b/lua/pac3/editor/client/init.lua
@@ -86,6 +86,7 @@ pace.ActivePanels = pace.ActivePanels or {}
pace.Editor = NULL
local remember = CreateConVar("pac_editor_remember_position", "1", {FCVAR_ARCHIVE}, "Remember PAC3 editor position on screen")
+local remember_divider = CreateConVar("pac_editor_remember_divider_height", "0", {FCVAR_ARCHIVE}, "Remember PAC3 editor's vertical divider position")
local positionMode = CreateConVar("pac_editor_position_mode", "0", {FCVAR_ARCHIVE}, "Editor position mode. 0 - Left, 1 - middle, 2 - Right. Has no effect if pac_editor_remember_position is true")
local showCameras = CreateConVar("pac_show_cameras", "1", {FCVAR_ARCHIVE}, "Show the PAC cameras of players using the editor")
local showInEditor = CreateConVar("pac_show_in_editor", "1", {FCVAR_ARCHIVE}, "Show the 'In PAC3 Editor' text above players using the editor")
@@ -135,6 +136,15 @@ function pace.OpenEditor()
end
end
+ if remember_divider:GetBool() then
+ pace.vertical_div_height = pace.vertical_div_height or ScrH()/1.4
+
+ timer.Simple(0, function()
+ editor.div:SetTopHeight(pace.vertical_div_height)
+ end)
+
+ end
+
if ctp and ctp.Disable then
ctp:Disable()
end
@@ -211,7 +221,7 @@ function pace.Panic()
if ent:IsValid() then
ent.pac_onuse_only = nil
ent.pac_onuse_only_check = nil
- hook.Remove('pace_OnUseOnlyUpdates', ent)
+ pac.RemoveHook("pace_OnUseOnlyUpdates", ent)
end
end
end
@@ -303,8 +313,8 @@ do
local up = Vector(0,0,10000)
- hook.Add("HUDPaint", "pac_in_editor", function()
- for _, ply in ipairs(player.GetAll()) do
+ pac.AddHook("HUDPaint", "in_editor", function()
+ for _, ply in player.Iterator() do
if ply ~= pac.LocalPlayer and ply:GetNW2Bool("pac_in_editor") then
if showCameras:GetInt() == 1 then
diff --git a/lua/pac3/editor/client/logic.lua b/lua/pac3/editor/client/logic.lua
index 329928faa..92026aca8 100644
--- a/lua/pac3/editor/client/logic.lua
+++ b/lua/pac3/editor/client/logic.lua
@@ -70,6 +70,7 @@ function pace.OnOpenEditor()
end
function pace.OnCloseEditor()
+ pace.FlushInfoPopups()
pace.EnableView(false)
pace.StopSelect()
pace.SafeRemoveSpecialPanel()
diff --git a/lua/pac3/editor/client/menu_bar.lua b/lua/pac3/editor/client/menu_bar.lua
index 15f8f5588..c348eac3a 100644
--- a/lua/pac3/editor/client/menu_bar.lua
+++ b/lua/pac3/editor/client/menu_bar.lua
@@ -55,6 +55,11 @@ local function populate_pac(menu)
function() pace.ShowWiki(pace.WikiURL .. "Beginners-FAQ") end
):SetImage(pace.MiscIcons.info)
+ help:AddOption(
+ L"PAC3 Wiki",
+ function() pace.ShowWiki("https://wiki.pac3.info/start") end
+ ):SetImage(pace.MiscIcons.info)
+
do
local chat_pnl = help:AddOption(
L"Discord / PAC3 Chat",
@@ -75,17 +80,103 @@ local function populate_pac(menu)
version_pnl:SetImage(pace.MiscIcons.info)
version:AddOption(version_string)
+
+ version:AddOption("local update changelogs", function() pac.OpenMOTD("local_changelog") end)
+ version:AddOption("external commit history", function() pac.OpenMOTD("commit_history") end)
+ version:AddOption("major update news (combat update)", function() pac.OpenMOTD("combat_update") end)
end
+
+
help:AddOption(
L"about",
function() pace.ShowAbout() end
):SetImage(pace.MiscIcons.about)
end
+ do
+ if cookie.GetNumber("pac3_new_features_review") == nil then cookie.Set("pac3_new_features_review", 1) end
+ if cookie.GetNumber("pac3_new_features_review") ~= 0 then
+ local experimentals, pnl_exp = menu:AddSubMenu(L"Discover new features", function()
+ Derma_Query("Do you wish to remove \"Discover new features\" from the pac tab?", "New feature review",
+ "remove", function() cookie.Set("pac3_new_features_review", 0) pace.CloseEditor() pace.OpenEditor() pace.RefreshTree() end,
+ "cancel", function() end)
+ end)
+ experimentals.GetDeleteSelf = function() return false end
+ pnl_exp:SetImage("icon16/medal_gold_1.png") pnl_exp:SetTooltip("You can hide this menu by clicking it for the prompt.")
+
+ local pnl = experimentals:AddOption("Bookmark favorite assets (models, sounds, materials)") pnl:SetIcon("icon16/cart_go.png") pnl:SetTooltip("Right click on the text fields to access your favorites.\nRight click on asset browser items/lines to set a single favorite\nThe option to favorite a series may pop up if there's a number.\nSelect, then right click on the folder in the directory tree to favorite a folder")
+ pnl = experimentals:AddOption("Customizable editor: reorder menu actions and custom shortcuts", function() pace.OpenSettings("Editor menu Settings") end) pnl:SetIcon("icon16/table_refresh.png") pnl:SetTooltip("You can define your editor actions in any order, leave some out, and there are new actions.\nShortcuts are also configurable.")
+ pnl = experimentals:AddOption("Customizable editor: custom part categories", function() pace.OpenSettings("Editor menu Settings 2") end) pnl:SetIcon("icon16/application_view_list.png") pnl:SetTooltip("categorize parts with custom categories")
+ local popups_tutorials, popups_pnl = experimentals:AddSubMenu("popups and tutorials") popups_pnl:SetImage("icon16/help.png") popups_pnl:SetTooltip("default shortut : F1")
+ popups_tutorials.GetDeleteSelf = function() return false end
+ popups_tutorials:AddOption("part tutorials (F1)", function() pace.current_part:AttachEditorPopup() end):SetIcon("icon16/help.png")
+ popups_tutorials:AddOption("proxy tutorials", function()
+ if pace.current_part.ClassName == "proxy" then
+ pace.current_part:AttachEditorPopup()
+ else
+ local has_proxy = false
+ local proxy_found
+ for i,v in pairs(pac.GetLocalParts()) do
+ if v.ClassName == "proxy" then
+ has_proxy = true
+ proxy_found = v
+ end
+ end
+ if has_proxy then
+ proxy_found:AttachEditorPopup()
+ else
+ pace.FlashNotification("There were no proxy parts found in the outfit.")
+ end
+ end
+ end):SetIcon("icon16/calculator.png")
+ local popup_cfg, popups_pnl2 = popups_tutorials:AddSubMenu("Configure popups", pace.OpenPopupConfig)
+ popups_pnl2:SetImage("icon16/color_wheel.png")
+ popup_cfg.GetDeleteSelf = function() return false end
+ popup_cfg:AddOption("Open popup config", pace.OpenPopupConfig):SetImage("icon16/color_wheel.png")
+ popup_cfg:AddOption("preset: day mode", function()
+ GetConVar("pac_popups_base_alpha"):SetString("255")
+ GetConVar("pac_popups_base_color"):SetString("255 255 255")
+ GetConVar("pac_popups_fade_alpha"):SetString("0")
+ GetConVar("pac_popups_fade_color"):SetString("255 255 255")
+ GetConVar("pac_popups_text_color"):SetString("40 40 40")
+ end):SetImage("icon16/contrast.png")
+ popup_cfg:AddOption("preset: night mode", function()
+ GetConVar("pac_popups_base_alpha"):SetString("255")
+ GetConVar("pac_popups_base_color"):SetString("40 40 40")
+ GetConVar("pac_popups_fade_alpha"):SetString("0")
+ GetConVar("pac_popups_fade_color"):SetString("0 0 0")
+ GetConVar("pac_popups_text_color"):SetString("255 255 255")
+ end):SetImage("icon16/contrast.png")
+ local popup_pref_mode, pnlppm = popup_cfg:AddSubMenu("prefered location", function() end)
+ pnlppm:SetImage("icon16/layout_header.png")
+ popup_pref_mode.GetDeleteSelf = function() return false end
+ popup_pref_mode:AddOption(L"parts on viewport", function() RunConsoleCommand("pac_popups_preferred_location", "part world") end):SetImage('icon16/camera.png')
+ popup_pref_mode:AddOption(L"part label on tree", function() RunConsoleCommand("pac_popups_preferred_location", "pac tree label") end):SetImage('icon16/layout_content.png')
+ popup_pref_mode:AddOption(L"menu bar", function() RunConsoleCommand("pac_popups_preferred_location", "menu bar") end):SetImage('icon16/layout_header.png')
+ popup_pref_mode:AddOption(L"cursor", function() RunConsoleCommand("pac_popups_preferred_location", "cursor") end):SetImage('icon16/mouse.png')
+ popup_pref_mode:AddOption(L"tracking cursor", function() RunConsoleCommand("pac_popups_preferred_location", "tracking cursor") end):SetImage('icon16/mouse_add.png')
+ popup_pref_mode:AddOption(L"screen", function() RunConsoleCommand("pac_popups_preferred_location", "screen") end):SetImage('icon16/monitor.png')
+
+
+ pnl = experimentals:AddOption("Bulk Select : " .. GetConVar("pac_bulk_select_key"):GetString() .. " + click to select; operations are in the part menu") pnl:SetIcon("icon16/table_multiple.png") pnl:SetTooltip("Bulk Select selects multiple parts to do operations quickly.\nIt has an order. The order of selection can matter for some operations like Bulk Morph Property.")
+ pnl = experimentals:AddOption("Morph properties on bulk select", pace.BulkMorphProperty) pnl:SetIcon("icon16/chart_line.png") pnl:SetTooltip("Once you have selected parts with Bulk Select, set variables gradually.\nIt can achieve color fades across multiple parts.\nThe order of selection matters.")
+ pnl = experimentals:AddOption("Arraying menu : select a matrix part and a stackable part", function() pace.OpenArrayingMenu(pace.current_part) end) pnl:SetIcon("icon16/shape_group.png") pnl:SetTooltip("Select an origin/matrix part before opening the menu.\nThen select an arrayed part\nThus you can quickly place models in a circle for example.\nBoth the matrix and arrayed part need to be movables.")
+ pnl = experimentals:AddOption("Process by Criteria", function()
+ for i,v in pairs(pace.Tools) do
+ if v.name == (L"Process by Criteria") then
+ v.callback(pace.current_part)
+ end
+ end
+ end) pnl:SetIcon("icon16/text_list_numbers.png") pnl:SetTooltip("Write criteria to process parts.\nThis is only useful if you have lots of parts with a certain number or text you want to replace in bulk.\nFor example you can mass replace events of one type with another, and set arguments to match.")
+ end
+ end
+
do
menu:AddOption(L"exit", function() pace.CloseEditor() end):SetImage(pace.MiscIcons.exit)
end
+
+
end
local function populate_view(menu)
@@ -101,10 +192,155 @@ end
local function populate_options(menu)
menu:AddOption(L"settings", function() pace.OpenSettings() end)
+
+ menu:AddCVar(L"Keyboard shortcuts: Legacy mode", "pac_editor_shortcuts_legacy_mode", "1", "0")
menu:AddCVar(L"inverse collapse/expand controls", "pac_reverse_collapse", "1", "0")
menu:AddCVar(L"enable shift+move/rotate clone", "pac_grab_clone", "1", "0")
+
menu:AddCVar(L"remember editor position", "pac_editor_remember_position", "1", "0")
+ menu:AddCVar(L"remember divider position", "pac_editor_remember_divider_height", "1", "0")
+ menu:AddCVar(L"remember editor width", "pac_editor_remember_width", "1", "0")
+
+ menu:AddSpacer()
+
+ local menu1, pnl = menu:AddSubMenu(L"double click actions") pnl:SetIcon("icon16/cursor.png")
+ menu1.GetDeleteSelf = function() return false end
+ local menu2, pnl = menu1:AddSubMenu(L"generic") pnl:SetIcon("icon16/world.png")
+ menu2.GetDeleteSelf = function() return false end
+ menu2:AddOption("expand / collapse", function() RunConsoleCommand("pac_doubleclick_action", "expand") end):SetImage('icon16/arrow_down.png')
+ menu2:AddOption("rename", function() RunConsoleCommand("pac_doubleclick_action", "rename") end):SetImage('icon16/text_align_center.png')
+ menu2:AddOption("write notes", function() RunConsoleCommand("pac_doubleclick_action", "notes") end):SetImage('icon16/page_white_edit.png')
+ menu2:AddOption("show / hide", function() RunConsoleCommand("pac_doubleclick_action", "showhide") end):SetImage('icon16/clock_red.png')
+ menu2:AddOption("only when specifed actions exist", function() RunConsoleCommand("pac_doubleclick_action", "specific_only") end):SetImage('icon16/application_xp_terminal.png')
+ menu2:AddOption("none", function() RunConsoleCommand("pac_doubleclick_action", "none") end):SetImage('icon16/collision_off.png')
+ local menu2, pnl = menu1:AddSubMenu(L"specific") pnl:SetIcon("icon16/application_xp_terminal.png")
+ menu2.GetDeleteSelf = function() return false end
+ menu2:AddOption("use generic actions only", function() RunConsoleCommand("pac_doubleclick_action_specified", "0") end):SetImage('icon16/world.png')
+ menu2:AddOption("use specific actions when available", function() RunConsoleCommand("pac_doubleclick_action_specified", "1") end):SetImage('icon16/cog.png')
+ menu2:AddOption("use even more specific actions (events)", function() RunConsoleCommand("pac_doubleclick_action_specified", "2") end):SetImage('icon16/clock.png')
+
+ menu:AddCVar(L"ask before loading autoload", "pac_prompt_for_autoload", "1", "0")
+
+ local prop_pac_load_mode, pnlpplm = menu:AddSubMenu("(singleplayer only) How to handle prop/npc outfits", function() end)
+ prop_pac_load_mode.GetDeleteSelf = function() return false end
+ pnlpplm:SetImage("icon16/transmit.png")
+ prop_pac_load_mode:AddOption(L"Load without queuing", function() RunConsoleCommand("pac_autoload_preferred_prop", "0") end)
+ prop_pac_load_mode:AddOption(L"Queue parts if there's only one group", function() RunConsoleCommand("pac_autoload_preferred_prop", "1") end)
+ prop_pac_load_mode:AddOption(L"Queue parts if there's one or more groups", function() RunConsoleCommand("pac_autoload_preferred_prop", "2") end)
+
menu:AddCVar(L"show parts IDs", "pac_show_uniqueid", "1", "0")
+
+ local halos, pnlh = menu:AddSubMenu("configure hover halo highlights", function() end)
+ halos.GetDeleteSelf = function() return false end
+ pnlh:SetImage("icon16/shading.png")
+ halos:AddCVar(L"disable hover halos", "pac_hover_color", "none", "255 255 255")
+ halos:AddOption("object limit (performance)", function()
+ Derma_StringRequest("pac_hover_halo_limit ", "how many objects can halo at once?", GetConVarNumber("pac_hover_halo_limit"), function(val) RunConsoleCommand("pac_hover_halo_limit", val) end)
+ end):SetImage("icon16/sitemap.png")
+ halos:AddOption("pulse rate", function()
+ Derma_StringRequest("pac_hover_pulserate", "how fast to pulse?", GetConVarNumber("pac_hover_pulserate"), function(val) RunConsoleCommand("pac_hover_pulserate", val) end)
+ end):SetImage("icon16/time.png")
+
+ halos:AddOption("How it reacts to bulk select", function()
+ local bulk_key_option_str = "bulk select key (current bind:" .. GetConVar("pac_bulk_select_key"):GetString() .. ")"
+ Derma_Query("What keys should trigger the hover halo on bulk select?","pac_bulk_select_halo_mode",
+ "passive",function() RunConsoleCommand("pac_bulk_select_halo_mode", 1) end,
+ bulk_key_option_str, function() RunConsoleCommand("pac_bulk_select_halo_mode", 2) end,
+ "control", function() RunConsoleCommand("pac_bulk_select_halo_mode", 3) end,
+ "shift", function() RunConsoleCommand("pac_bulk_select_halo_mode", 4) end
+ )
+ end):SetImage("icon16/table_multiple.png")
+ halos:AddOption("Do not highlight bulk select", function()
+ RunConsoleCommand("pac_bulk_select_halo_mode", "0")
+ end):SetImage("icon16/table_delete.png")
+
+ local halos_color, pnlhclr = halos:AddSubMenu("hover halo color", function() end)
+ pnlhclr:SetImage("icon16/color_wheel.png")
+ halos_color.GetDeleteSelf = function() return false end
+ halos_color:AddOption(L"none (disable halos)", function() RunConsoleCommand("pac_hover_color", "none") end):SetImage('icon16/page_white.png')
+ halos_color:AddOption(L"white (default)", function() RunConsoleCommand("pac_hover_color", "255 255 255") end):SetImage('icon16/bullet_white.png')
+ halos_color:AddOption(L"color (opens a menu)", function()
+ local clr_frame = vgui.Create("DFrame")
+ clr_frame:SetSize(300,200) clr_frame:Center()
+ local clr_pnl = vgui.Create("DColorMixer", clr_frame)
+ clr_frame:SetSize(300,200) clr_pnl:Dock(FILL)
+ clr_frame:RequestFocus()
+ function clr_pnl:ValueChanged(col)
+ GetConVar("pac_hover_color"):SetString(col.r .. " " .. col.g .. " " .. col.b)
+ end
+ end):SetImage('icon16/color_swatch.png')
+ halos_color:AddOption(L"ocean", function() RunConsoleCommand("pac_hover_color", "ocean") end):SetImage('icon16/bullet_blue.png')
+ halos_color:AddOption(L"funky", function() RunConsoleCommand("pac_hover_color", "funky") end):SetImage('icon16/color_wheel.png')
+ halos_color:AddOption(L"rave", function() RunConsoleCommand("pac_hover_color", "rave") end):SetImage('icon16/color_wheel.png')
+ halos_color:AddOption(L"rainbow", function() RunConsoleCommand("pac_hover_color", "rainbow") end):SetImage('icon16/rainbow.png')
+
+ local popups, pnlp = menu:AddSubMenu("configure editor popups", function() end)
+ popups.GetDeleteSelf = function() return false end
+ pnlp:SetImage("icon16/comment.png")
+ popups:AddCVar(L"enable editor popups", "pac_popups_enable", "1", "0")
+ popups:AddCVar(L"don't kill popups on autofade", "pac_popups_preserve_on_autofade", "1", "0")
+ popups:AddOption("Configure popups appearance", function() pace.OpenPopupConfig() end):SetImage('icon16/color_wheel.png')
+ popups:AddOption("preset: day mode", function()
+ GetConVar("pac_popups_base_alpha"):SetString("255")
+ GetConVar("pac_popups_base_color"):SetString("255 255 255")
+ GetConVar("pac_popups_fade_alpha"):SetString("0")
+ GetConVar("pac_popups_fade_color"):SetString("255 255 255")
+ GetConVar("pac_popups_text_color"):SetString("40 40 40")
+ end):SetImage("icon16/contrast.png")
+ popups:AddOption("preset: night mode", function()
+ GetConVar("pac_popups_base_alpha"):SetString("255")
+ GetConVar("pac_popups_base_color"):SetString("40 40 40")
+ GetConVar("pac_popups_fade_alpha"):SetString("0")
+ GetConVar("pac_popups_fade_color"):SetString("0 0 0")
+ GetConVar("pac_popups_text_color"):SetString("255 255 255")
+ end):SetImage("icon16/contrast.png")
+ local popup_pref_mode, pnlppm = popups:AddSubMenu("prefered location", function() end)
+ pnlppm:SetImage("icon16/layout_header.png")
+ popup_pref_mode.GetDeleteSelf = function() return false end
+ popup_pref_mode:AddOption(L"parts on viewport", function() RunConsoleCommand("pac_popups_preferred_location", "part world") end):SetImage('icon16/camera.png')
+ popup_pref_mode:AddOption(L"part label on tree", function() RunConsoleCommand("pac_popups_preferred_location", "pac tree label") end):SetImage('icon16/layout_content.png')
+ popup_pref_mode:AddOption(L"menu bar", function() RunConsoleCommand("pac_popups_preferred_location", "menu bar") end):SetImage('icon16/layout_header.png')
+ popup_pref_mode:AddOption(L"cursor", function() RunConsoleCommand("pac_popups_preferred_location", "cursor") end):SetImage('icon16/mouse.png')
+ popup_pref_mode:AddOption(L"screen", function() RunConsoleCommand("pac_popups_preferred_location", "screen") end):SetImage('icon16/monitor.png')
+
+ menu:AddOption(L"configure event wheel", pace.ConfigureEventWheelMenu):SetImage("icon16/color_wheel.png")
+
+ local copilot, pnlc = menu:AddSubMenu("configure editor copilot", function() end)
+ copilot.GetDeleteSelf = function() return false end
+ pnlc:SetImage("icon16/award_star_gold_3.png")
+ copilot:AddCVar(L"show info popup when changing an event's type", "pac_copilot_make_popup_when_selecting_event", "1", "0")
+ copilot:AddCVar(L"auto-focus on the main property when creating some parts", "pac_copilot_auto_focus_main_property_when_creating_part","1","0")
+ copilot:AddCVar(L"auto-setup a command event when entering a name as an event type", "pac_copilot_auto_setup_command_events", "1", "0")
+ copilot:AddCVar(L"open asset browser when creating some parts", "pac_copilot_open_asset_browser_when_creating_part", "1", "0")
+ copilot:AddCVar(L"disable the editor view when creating a camera part", "pac_copilot_force_preview_cameras", "1", "0")
+ local copilot_add_part_search_menu, pnlaps = copilot:AddSubMenu("configure the searchable add part menu", function() end)
+ pnlaps:SetImage("icon16/add.png")
+ copilot_add_part_search_menu.GetDeleteSelf = function() return false end
+ copilot_add_part_search_menu:AddOption(L"No copilot", function() RunConsoleCommand("pac_copilot_partsearch_depth", "-1") end):SetImage('icon16/page_white.png')
+ copilot_add_part_search_menu:AddOption(L"automatically select a text field after creating the part (e.g. event type)", function() RunConsoleCommand("pac_copilot_partsearch_depth", "0") end):SetImage('icon16/layout_edit.png')
+ copilot_add_part_search_menu:AddOption(L"open another quick list menu (event types, favorite models...)", function() RunConsoleCommand("pac_copilot_partsearch_depth", "1") end):SetImage('icon16/application_view_list.png')
+
+ local combat_consents, pnlcc = menu:AddSubMenu("pac combat consents", function() end)
+ combat_consents.GetDeleteSelf = function() return false end
+ pnlcc:SetImage("icon16/joystick.png")
+
+ local npc_pref = combat_consents:AddOption(L"Level of protection for friendly NPCs", function()
+ Derma_Query("Prevent friendly fire against NPCs? (damage zone and hitscan)", "NPC relationship preferences (pac_client_npc_exclusion_consent = " .. GetConVar("pac_client_npc_exclusion_consent"):GetInt() .. ")",
+ "Don't protect (0)", function() GetConVar("pac_client_npc_exclusion_consent"):SetInt(0) end,
+ "Protect friendly NPCs (1)", function() GetConVar("pac_client_npc_exclusion_consent"):SetInt(1) end,
+ "Protect friendly and neutral NPCs (2)", function() GetConVar("pac_client_npc_exclusion_consent"):SetInt(2) end,
+ "cancel")
+ end)
+ npc_pref:SetImage("icon16/group.png")
+ npc_pref:SetTooltip("\"Friendliness\" is based on an NPC's Disposition toward you: Error&Hate, Fear&Neutral, Like")
+
+ combat_consents:AddCVar(L"damage_zone part (area damage)", "pac_client_damage_zone_consent", "1", "0")
+ combat_consents:AddCVar(L"hitscan part (bullets)", "pac_client_hitscan_consent", "1", "0")
+ combat_consents:AddCVar(L"force part (physics forces)", "pac_client_force_consent", "1", "0")
+ combat_consents:AddCVar(L"lock part's grab (can take control of your position and eye angles)", "pac_client_grab_consent", "1", "0")
+ combat_consents:AddCVar(L"lock part's grab calcview (can take control of your view position)", "pac_client_lock_camera_consent", "1", "0"):SetTooltip("You're still not immune to it changing your eye angles.\nCalcviews are a different thing than eye angles.")
+
+
menu:AddSpacer()
menu:AddOption(L"position grid size", function()
Derma_StringRequest(L"position grid size", L"size in units:", GetConVarNumber("pac_grid_pos_size"), function(val)
@@ -123,6 +359,10 @@ local function populate_options(menu)
menu:AddCVar(L"enable language identifier in text fields", "pac_editor_languageid", "1", "0")
pace.AddLanguagesToMenu(menu)
pace.AddFontsToMenu(menu)
+ menu:AddCVar(L"Use the new PAC4.5 icon", "pac_icon", "1", "0")
+ if cookie.GetNumber("pac3_new_features_review") == 0 then
+ menu:AddOption("re-show new features review", function() cookie.Set("pac3_new_features_review", 1) pace.CloseEditor() pace.OpenEditor() pace.RefreshTree() end):SetIcon("icon16/medal_gold_1.png")
+ end
menu:AddSpacer()
@@ -132,11 +372,156 @@ local function populate_options(menu)
rendering:AddCVar(L"no outfit reflections", "pac_optimization_render_once_per_frame", "1", "0")
end
+local function get_events()
+ local events = {}
+ for k,v in pairs(pac.GetLocalParts()) do
+ if v.ClassName == "event" then
+ local e = v:GetEvent()
+ if e == "command" then
+ local cmd, time, hide = v:GetParsedArgumentsForObject(v.Events.command)
+ local b = false
+ events[cmd] = pac.LocalPlayer.pac_command_events[cmd] and pac.LocalPlayer.pac_command_events[cmd].on == 1 or false
+ end
+ end
+ end
+ return events
+end
+
local function populate_player(menu)
local pnl = menu:AddOption(L"t pose", function() pace.SetTPose(not pace.GetTPose()) end):SetImage("icon16/user_go.png")
menu:AddOption(L"reset eye angles", function() pace.ResetEyeAngles() end):SetImage("icon16/user_delete.png")
menu:AddOption(L"reset zoom", function() pace.ResetZoom() end):SetImage("icon16/magnifier.png")
+ local seq_cmdmenu, pnl2 = menu:AddSubMenu(L"sequenced command events") pnl2:SetImage("icon16/clock.png")
+ seq_cmdmenu.GetDeleteSelf = function() return false end
+
+ local full_cmdmenu, pnl3 = menu:AddSubMenu(L"full list of command events") pnl3:SetImage("icon16/clock_play.png")
+ full_cmdmenu.GetDeleteSelf = function() return false end
+
+ local full_proxymenu, pnl4 = menu:AddSubMenu(L"full list of command proxies") pnl4:SetImage("icon16/calculator.png")
+ full_proxymenu.GetDeleteSelf = function() return false end
+
+ local rebuild_events_menu
+ local rebuild_seq_menu
+ local rebuild_proxies_menu
+
+
+ local rebuild_seq_menu = function()
+ seq_cmdmenu:Clear()
+ if pac.LocalPlayer.pac_command_event_sequencebases == nil then return end
+ for cmd, tbl in pairs(pac.LocalPlayer.pac_command_event_sequencebases) do
+ if tbl.max ~= 0 then
+ local submenu, pnl3 = seq_cmdmenu:AddSubMenu(cmd) pnl3:SetImage("icon16/clock_red.png")
+ submenu.GetDeleteSelf = function() return false end
+ if tbl.min == nil then continue end
+ for i=tbl.min,tbl.max,1 do
+ local func_sequenced = function()
+ RunConsoleCommand("pac_event_sequenced", cmd, "set", tostring(i,0)) rebuild_events_menu()
+ end
+ local option = submenu:AddOption(cmd..i,func_sequenced) option:SetIsCheckable(true) option:SetRadio(true)
+ if i == tbl.current then option:SetChecked(true) end
+ if pac.LocalPlayer.pac_command_events[cmd..i] then
+ if pac.LocalPlayer.pac_command_events[cmd..i].on == 1 then
+ option:SetChecked(true)
+ end
+ end
+ function option:SetChecked(b)
+ if ( self:GetChecked() != b ) then
+ self:OnChecked( b )
+ end
+ self.m_bChecked = b
+ if b then func_sequenced() end
+ timer.Simple(0.4, rebuild_events_menu)
+ end
+ end
+ end
+ end
+ end
+
+ if pac.LocalPlayer.pac_command_event_sequencebases then
+ if table.Count(pac.LocalPlayer.pac_command_event_sequencebases) > 0 then
+ rebuild_seq_menu()
+ end
+ end
+
+ rebuild_events_menu = function()
+ full_cmdmenu:Clear()
+ for cmd, b in SortedPairs(get_events()) do
+ local option = full_cmdmenu:AddOption(cmd,function() RunConsoleCommand("pac_event", cmd, "2") end) option:SetIsCheckable(true)
+ if b then option:SetChecked(true) end
+ function option:OnChecked(b)
+ if b then RunConsoleCommand("pac_event", cmd, "1") else RunConsoleCommand("pac_event", cmd, "0") end rebuild_seq_menu()
+ end
+ if pace.command_colors == nil then continue end
+ if pace.command_colors[cmd] ~= nil then
+ local clr = Color(unpack(string.Split(pace.command_colors[cmd]," ")))
+ clr.a = 100
+ option.PaintOver = function(_,w,h) surface.SetDrawColor(clr) surface.DrawRect(0,0,w,h) end
+ end
+ end
+ end
+
+ rebuild_proxies_menu = function()
+ full_proxymenu:Clear()
+ if pac.LocalPlayer.pac_proxy_events == nil then return end
+ for cmd, tbl in SortedPairs(pac.LocalPlayer.pac_proxy_events) do
+ local num = tbl.x
+ if tbl.y ~= 0 or tbl.z ~= 0 then
+ num = tbl.x .. " " .. tbl.y .. " " .. tbl.z
+ end
+ full_proxymenu:AddOption(cmd .. " : " .. num,function()
+ Derma_StringRequest("Set new value for pac_proxy " .. cmd, "please input a number or spaced vector-notation.\n++ and -- notation is also supported for any component.\nit shall be used in a proxy expression as command(\""..cmd.."\")", num,
+ function(str)
+ local args = string.Split(str, " ")
+ RunConsoleCommand("pac_proxy", cmd, unpack(args))
+ timer.Simple(0.4, rebuild_proxies_menu)
+ end)
+ end)
+ end
+ end
+
+ if pac.LocalPlayer.pac_command_events then
+ if table.Count(pac.LocalPlayer.pac_command_events) > 0 then
+ rebuild_events_menu()
+ end
+ end
+ if pac.LocalPlayer.pac_proxy_events then
+ if table.Count(pac.LocalPlayer.pac_proxy_events) > 0 then
+ rebuild_proxies_menu()
+ end
+ end
+
+ function pnl2:Think()
+ if self:IsHovered() then
+ if not self.isrebuilt then
+ rebuild_events_menu()
+ self.isrebuilt = true
+ end
+ else
+ self.isrebuilt = false
+ end
+ end
+ function pnl3:Think()
+ if self:IsHovered() then
+ if not self.isrebuilt then
+ rebuild_seq_menu()
+ self.isrebuilt = true
+ end
+ else
+ self.isrebuilt = false
+ end
+ end
+ function pnl4:Think()
+ if self:IsHovered() then
+ if not self.isrebuilt then
+ rebuild_proxies_menu()
+ self.isrebuilt = true
+ end
+ else
+ self.isrebuilt = false
+ end
+ end
+
-- this should be in pacx but it's kinda stupid to add a hook just to populate the player menu
-- make it more generic
if pacx and pacx.GetServerModifiers then
@@ -149,6 +534,19 @@ local function populate_player(menu)
end
end
+function pace.PopulateMenuBarTab(menu, tab)
+ if tab == "pac" then
+ populate_pac(menu)
+ elseif tab == "player" then
+ populate_player(menu)
+ elseif tab == "options" then
+ populate_options(menu)
+ elseif tab == "view" then
+ populate_view(menu)
+ end
+ --timer.Simple(0.3, function() menu:RequestFocus() end)
+end
+
function pace.OnMenuBarPopulate(bar)
for k,v in pairs(bar.Menus) do
v:Remove()
@@ -161,6 +559,11 @@ function pace.OnMenuBarPopulate(bar)
pace.AddToolsToMenu(bar:AddMenu(L"tools"))
bar:RequestFocus(true)
+ --[[timer.Simple(0.2, function()
+ if IsValid(bar) then
+ bar:RequestFocus(true)
+ end
+ end)]]
end
function pace.OnOpenMenu()
diff --git a/lua/pac3/editor/client/panels/editor.lua b/lua/pac3/editor/client/panels/editor.lua
index dab095bdb..9cae0b1df 100644
--- a/lua/pac3/editor/client/panels/editor.lua
+++ b/lua/pac3/editor/client/panels/editor.lua
@@ -18,6 +18,25 @@ local zoom_persistent = CreateClientConVar("pac_zoom_persistent", 0, true, false
local zoom_mousewheel = CreateClientConVar("pac_zoom_mousewheel", 0, true, false, 'Enable zooming with mouse wheel.')
local zoom_smooth = CreateClientConVar("pac_zoom_smooth", 0, true, false, 'Enable smooth zooming.')
+local remember_divider = CreateConVar("pac_editor_remember_divider_height", "0", {FCVAR_ARCHIVE}, "Remember PAC3 editor's vertical divider position")
+local remember_width = CreateConVar("pac_editor_remember_width", "0", {FCVAR_ARCHIVE}, "Remember PAC3 editor's width")
+
+function pace.RefreshZoomBounds(zoomslider)
+ if pace.Editor then
+ if not zoomslider then
+ zoomslider = pace.Editor.zoomslider
+ end
+ if pace.camera_orthographic then
+ zoomslider:SetMin(-10000)
+ zoomslider:SetMax(10000)
+ else
+ zoomslider:SetMin(0)
+ zoomslider:SetMax(pace.max_fov)
+ timer.Simple(0, function() zoomslider:SetValue(math.Clamp(pace.ViewFOV, 0, pace.max_fov)) end)
+ end
+ end
+end
+
function PANEL:Init()
self:SetTitle("")
self:SetSizable(true)
@@ -101,6 +120,54 @@ function PANEL:Init()
self.smoothlabel:SetWrap(true)
self.smoothlabel:SetAutoStretchVertical(true)
+ self.limitfovcheckbox = vgui.Create("DCheckBoxLabel", self.zoomsettings)
+ self.limitfovcheckbox:SetText("Expanded FOV")
+ self.limitfovcheckbox:SetChecked(pace.max_fov ~= 100)
+ self.limitfovcheckbox:Dock(TOP)
+ self.limitfovcheckbox:SetDark(true)
+ self.limitfovcheckbox:DockMargin(0,SETTING_MARGIN_TOP,0,0)
+
+
+ self.orthocheckbox = vgui.Create("DCheckBoxLabel", self.zoomsettings)
+ self.orthocheckbox:SetText("Orthographic")
+ self.orthocheckbox:Dock(TOP)
+ self.orthocheckbox:SetDark(true)
+ self.orthocheckbox:DockMargin(0,SETTING_MARGIN_TOP,0,0)
+ self.orthocheckbox:SetConVar("pac_camera_orthographic")
+ self.orthocheckbox:SetTooltip("Orthographic view projects parallel rays perpendicular to a rectangle. Instead of degrees, it is in terms of distance units (Hammer Units)\n\nThere are still —possibly engine-related— issues where objects and world geometry can disapear if looking from the wrong angle due to culling. Especially worse in tight spaces.")
+
+ self.ortholabel = vgui.Create("DLabel", self.zoomsettings)
+ self.ortholabel:Dock(TOP)
+ self.ortholabel:SetDark(true)
+ self.ortholabel:SetText("Enable orthographic view.")
+ self.ortholabel:SetWrap(true)
+ self.ortholabel:SetAutoStretchVertical(true)
+
+ self.ortho_nearz = vgui.Create("DNumSlider", self.zoomsettings)
+ self.ortho_nearz:Dock(TOP)
+ self.ortho_nearz:SetMin( 0 )
+ self.ortho_nearz:SetMax( 5000 )
+ self.ortho_nearz:SetDecimals( 1 )
+ self.ortho_nearz:SetText("NearZ")
+ self.ortho_nearz:SetDark(true)
+ self.ortho_nearz:SetDefaultValue( 0 )
+ self.ortho_nearz:SetValue( 0 )
+
+ self.ortho_farz = vgui.Create("DNumSlider", self.zoomsettings)
+ self.ortho_farz:Dock(TOP)
+ self.ortho_farz:SetMin( 0 )
+ self.ortho_farz:SetMax( 64000 )
+ self.ortho_farz:SetDecimals( 1 )
+ self.ortho_farz:SetText("FarZ")
+ self.ortho_farz:SetDark(true)
+ self.ortho_farz:SetDefaultValue( 64000 )
+ self.ortho_farz:SetValue( 64000 )
+ if not pace.camera_orthographic then
+ self.ortho_nearz:Hide()
+ self.ortho_farz:Hide()
+ end
+
+
self.sliderpanel = vgui.Create("DPanel", self.zoomframe)
self.sliderpanel:SetSize(180, 20)
self.sliderpanel:Dock(TOP)
@@ -108,10 +175,12 @@ function PANEL:Init()
self.zoomslider = vgui.Create("DNumSlider", self.sliderpanel)
self.zoomslider:DockPadding(4,0,0,0)
self.zoomslider:SetSize(200, 20)
- self.zoomslider:SetMin( 0 )
- self.zoomslider:SetMax( 100 )
self.zoomslider:SetDecimals( 0 )
self.zoomslider:SetText("Camera FOV")
+ if pace.camera_orthographic then
+ self.zoomslider:SetText("Ortho. Width")
+ end
+ pace.RefreshZoomBounds(self.zoomslider)
self.zoomslider:SetDark(true)
self.zoomslider:SetDefaultValue( 75 )
@@ -120,6 +189,15 @@ function PANEL:Init()
else
self.zoomslider:SetValue( 75 )
end
+ local zoomslider = self.zoomslider
+ function self.limitfovcheckbox:OnChange(b)
+ if b then
+ pace.max_fov = 179
+ else
+ pace.max_fov = 100
+ end
+ pace.RefreshZoomBounds(zoomslider)
+ end
self.btnClose.Paint = function() end
@@ -128,6 +206,13 @@ function PANEL:Init()
self:SetCookieName("pac3_editor")
self:SetPos(self:GetCookieNumber("x"), BAR_SIZE)
+ if remember_width:GetBool() then
+ self.init_w = math.max(self:GetCookieNumber("width"), 200)
+ end
+ if remember_divider:GetBool() then
+ pace.vertical_div_height = self:GetCookieNumber("y_divider")
+ end
+
self:MakeBar()
self.lastTopBarHover = 0
self.rendertime_data = {}
@@ -172,6 +257,14 @@ function PANEL:MakeBar()
end
function PANEL:OnRemove()
+ if remember_divider:GetBool() then
+ pace.vertical_div_height = self.div:GetTopHeight()
+ end
+
+ if remember_width:GetBool() then
+ pace.editor_width = math.max(self:GetWide(), 200)
+ end
+
if self.menu_bar:IsValid() then
self.menu_bar:Remove()
end
@@ -185,6 +278,10 @@ function PANEL:OnRemove()
end
end
+function PANEL:IsLeft() --which side the editor is on.
+ return self:GetPos() + self:GetWide() / 2 < ScrW() / 2
+end
+
function PANEL:Think(...)
if not self.okay then return end
DFrame.Think(self, ...)
@@ -200,19 +297,34 @@ function PANEL:Think(...)
local bar = self.menu_bar
- self:SetTall(ScrH())
+
+ self:SetTall(ScrH() - (self.y_offset or 0))
local w = math.max(self:GetWide(), 200)
+
+ --wtf the GetWide isn't saved on Init??? I have to do this?
+ if self.init_w then
+ w = self.init_w
+ self.init_w = nil
+ end
self:SetWide(w)
- self:SetPos(math.Clamp(self:GetPos(), 0, ScrW() - w), 0)
+ self:SetPos(math.Clamp(self:GetPos(), 0, ScrW() - w), (self.y_offset or 0))
if x ~= self.last_x then
self:SetCookie("x", x)
self.last_x = x
end
+ if w ~= self.last_w then
+ self:SetCookie("width", w)
+ self.last_w = w
+ end
+ if pace.vertical_div_height ~= self.last_vertical_div_height then
+ self:SetCookie("y_divider", pace.vertical_div_height)
+ self.last_vertical_div_height = pace.vertical_div_height
+ end
if self.exit_button:IsValid() then
- if self:GetPos() + self:GetWide() / 2 < ScrW() / 2 then
+ if self:IsLeft() then
self.exit_button:SetPos(ScrW() - self.exit_button:GetWide() + 4, -4)
else
self.exit_button:SetPos(-4, -4)
@@ -244,6 +356,9 @@ function PANEL:Think(...)
self.zoomslider:SetValue(75)
pace.zoom_reset = nil
end
+ if pace.OverridingFOVSlider then
+ self.zoomslider:SetValue(pace.ViewFOV)
+ end
if zoom_smooth:GetInt() == 1 then
pace.SetZoom(self.zoomslider:GetValue(),true)
@@ -261,6 +376,8 @@ function PANEL:Think(...)
else
self.zoomsettings:SetVisible(false)
end
+
+
end
end
@@ -278,6 +395,9 @@ function PANEL:PerformLayout()
end
if self.old_part ~= pace.current_part then
+ if remember_divider:GetBool() then
+ pace.vertical_div_height = self.div:GetTopHeight()
+ end
self.div:InvalidateLayout()
self.bottom:PerformLayout()
pace.properties:PerformLayout()
@@ -292,10 +412,23 @@ function PANEL:PerformLayout()
local oldh = self.div:GetTopHeight()
if newh= 1 then
- self.div:SetTopHeight(newh)
+
+ if remember_divider:GetBool() then
+ if remember_divider:GetBool() then
+ self.div:SetTopHeight(pace.vertical_div_height)
+ else
+ self.div:SetTopHeight(newh)
+ end
+ else
+ self.div:SetTopHeight(newh)
+ end
end
end
end
diff --git a/lua/pac3/editor/client/panels/extra_properties.lua b/lua/pac3/editor/client/panels/extra_properties.lua
index fb2283d5a..de6c5e955 100644
--- a/lua/pac3/editor/client/panels/extra_properties.lua
+++ b/lua/pac3/editor/client/panels/extra_properties.lua
@@ -188,7 +188,7 @@ do -- part
if not self:IsValid() then return end
self:SetValue(part:GetUniqueID())
self.OnValueChanged(part)
- end)
+ end, self)
end
function PANEL:MoreOptionsRightClick(key)
@@ -219,6 +219,72 @@ do -- part
pace.RegisterPanel(PANEL)
end
+do -- custom animation frame event
+ local PANEL = {}
+
+ PANEL.ClassName = "properties_custom_animation_frame"
+ PANEL.Base = "pace_properties_part"
+
+ function PANEL:MoreOptionsLeftClick()
+ pace.CreateSearchList(
+ self,
+ self.CurrentKey,
+ L"custom animations",
+
+ function(list)
+ list:AddColumn(L"name")
+ list:AddColumn(L"id")
+ end,
+
+ function()
+ local output = {}
+ local parts = pac.GetLocalParts()
+
+ for i, part in pairs(parts) do
+
+ if part.ClassName == "custom_animation" then
+ local name = part.Name ~= "" and part.Name or "no name"
+ output[i] = name
+ end
+ end
+
+ return output
+ end,
+
+ function() return pace.current_part:GetProperty("animation") end,
+
+ function(list, key, val)
+ return list:AddLine(val, key)
+ end,
+
+ function(key, val) return val end,
+
+ function(key, val) return key end
+ )
+ end
+
+ function PANEL:MoreOptionsRightClick(key)
+ local menu = DermaMenu()
+
+ menu:MakePopup()
+
+ for _, part in pairs(pac.GetLocalParts()) do
+ if not part:HasParent() and part:GetShowInEditor() then
+ populate_part_menu(menu, part, function(part)
+ if not self:IsValid() then return end
+ if part.ClassName ~= "custom_animation" then return end
+ self:SetValue(part:GetUniqueID())
+ self.OnValueChanged(part:GetUniqueID())
+ end)
+ end
+ end
+
+ pace.FixMenu(menu)
+ end
+
+ pace.RegisterPanel(PANEL)
+end
+
do -- owner
local PANEL = {}
@@ -538,6 +604,168 @@ do -- sound
pace.RegisterPanel(PANEL)
end
+do --generic multiline text
+ local PANEL = {}
+
+ PANEL.ClassName = "properties_generic_multiline"
+ PANEL.Base = "pace_properties_base_type"
+
+ function PANEL:MoreOptionsLeftClick()
+ local pnl = vgui.Create("DFrame")
+ local DText = vgui.Create("DTextEntry", pnl)
+ local DButtonOK = vgui.Create("DButton", pnl)
+ DText:SetMaximumCharCount(50000)
+
+ pnl:SetSize(1200,800)
+ pnl:SetTitle("Long text with newline support for " .. self.CurrentKey .. ". If the text is too long, do not touch the label after this!")
+ pnl:SetPos(200, 100)
+ DButtonOK:SetText("OK")
+ DButtonOK:SetSize(80,20)
+ DButtonOK:SetPos(500, 775)
+ DText:SetPos(5,25)
+ DText:SetSize(1190,700)
+ DText:SetMultiline(true)
+ DText:SetContentAlignment(7)
+ pnl:MakePopup()
+ DText:RequestFocus()
+ DText:SetText(pace.current_part[self.CurrentKey])
+
+ DButtonOK.DoClick = function()
+ local str = DText:GetText()
+ pace.current_part[self.CurrentKey] = str
+ if pace.current_part.ClassName == "sound2" then
+ pace.current_part.AllPaths = str
+ pace.current_part:UpdateSoundsFromAll()
+ end
+ pace.PopulateProperties(pace.current_part)
+ pnl:Remove()
+ end
+ end
+
+ pace.RegisterPanel(PANEL)
+end
+
+
+local calcdrag_remove_pnl = CreateClientConVar("pac_luapad_calcdrag_removal", "1", true, false, "whether dragging view should remove the luapad")
+
+local lua_editor_txt = ""
+local lua_editor_previous_dimensions
+local lua_editor_fontsize = 16
+local function install_fontsize_buttons(frame, editor, add_execute, key)
+ frame.ignore_saferemovespecialpanel = not calcdrag_remove_pnl:GetBool()
+
+ local btn_fontplus = vgui.Create("DButton", frame) btn_fontplus:SetSize(20, 18) btn_fontplus:SetText("+")
+ local btn_fontminus = vgui.Create("DButton", frame) btn_fontminus:SetSize(20, 18) btn_fontminus:SetText("-")
+ function btn_fontplus:DoClick()
+ lua_editor_fontsize = math.Clamp(lua_editor_fontsize + 1, 6, 80)
+ surface.CreateFont("LuapadEditor", {font = "roboto mono", size = lua_editor_fontsize, weight = 400 } )
+ surface.CreateFont("LuapadEditor_Bold", {font = "roboto mono", size = lua_editor_fontsize, weight = 800})
+ surface.SetFont("LuapadEditor");
+ editor.FontWidth, editor.FontHeight = surface.GetTextSize(" ")
+ end
+ function btn_fontminus:DoClick()
+ lua_editor_fontsize = math.Clamp(lua_editor_fontsize - 1, 6, 80)
+ surface.CreateFont("LuapadEditor", {font = "roboto mono", size = lua_editor_fontsize, weight = 400 } )
+ surface.CreateFont("LuapadEditor_Bold", {font = "roboto mono", size = lua_editor_fontsize, weight = 800})
+ surface.SetFont("LuapadEditor");
+ editor.FontWidth, editor.FontHeight = surface.GetTextSize(" ")
+ end
+
+
+ local btn_remember_dimensions = vgui.Create("DButton", frame) btn_remember_dimensions:SetSize(18, 18) btn_remember_dimensions:SetImage("icon16/computer_link.png")
+ btn_remember_dimensions:SetTooltip("Remember winbdow size") btn_remember_dimensions:SetY(3)
+ function btn_remember_dimensions:DoClick()
+ if lua_editor_previous_dimensions == nil then
+ local x,y = frame:LocalToScreen()
+ lua_editor_previous_dimensions = {
+ x = x,
+ y = y,
+ wide = frame:GetWide(),
+ tall = frame:GetTall()
+ }
+ frame.special_title = "will remember position and size" timer.Simple(3, function() if not IsValid(frame) then return end frame.special_title = nil end)
+ else
+ lua_editor_previous_dimensions = nil
+ frame.special_title = "will not remember window position and size" timer.Simple(3, function() if not IsValid(frame) then return end frame.special_title = nil end)
+ end
+ end
+ if lua_editor_previous_dimensions then
+ btn_remember_dimensions:SetImage("icon16/computer_delete.png")
+ end
+
+ local btn_calcdrag_remove = vgui.Create("DButton", frame) btn_calcdrag_remove:SetSize(18, 18) btn_calcdrag_remove:SetImage("icon16/application_delete.png")
+ btn_calcdrag_remove:SetTooltip("Close window if dragging main view") btn_calcdrag_remove:SetY(3)
+ function btn_calcdrag_remove:DoClick()
+ calcdrag_remove_pnl:SetBool(not calcdrag_remove_pnl:GetBool())
+ frame.ignore_saferemovespecialpanel = not calcdrag_remove_pnl:GetBool()
+ if calcdrag_remove_pnl:GetBool() then
+ frame.special_title = "will remove window if dragging main view" timer.Simple(3, function() if not IsValid(frame) then return end frame.special_title = nil end)
+ else
+ frame.special_title = "will not remove window if dragging main view" timer.Simple(3, function() if not IsValid(frame) then return end frame.special_title = nil end)
+ end
+ end
+
+
+ local perflayout = frame.PerformLayout
+ btn_fontplus:SetY(3)
+ btn_fontminus:SetY(3)
+
+ if add_execute then
+ local btn_run = vgui.Create("DButton", frame) btn_run:SetSize(50, 18) btn_run:SetY(3)
+ btn_run:SetImage("icon16/bullet_go.png") btn_run:SetText(" run")
+ function btn_run:DoClick()
+ if key then
+ if key == "String" then
+ pace.current_part:SetString(editor:GetValue())
+ elseif key == "OnHideString" then
+ pace.current_part:SetOnHideString(editor:GetValue())
+ elseif key == "DelayedString" then
+ pace.current_part:SetDelayedString(editor:GetValue())
+ end
+ timer.Simple(0.2, function() pace.current_part:Execute(pace.current_part[key]) end)
+ else
+ pace.current_part:Execute()
+ end
+ end
+ function frame:PerformLayout()
+ if lua_editor_previous_dimensions then
+ local x,y = frame:LocalToScreen()
+ lua_editor_previous_dimensions = {
+ x = x,
+ y = y,
+ wide = frame:GetWide(),
+ tall = frame:GetTall()
+ }
+ end
+ btn_calcdrag_remove:SetX(self:GetWide() - 230 + 4)
+ btn_remember_dimensions:SetX(self:GetWide() - 210 + 4)
+ btn_run:SetX(self:GetWide() - 190 + 4)
+ btn_fontplus:SetX(self:GetWide() - 120 + 4)
+ btn_fontminus:SetX(self:GetWide() - 140 + 4)
+ perflayout(self)
+ end
+ frame:RequestFocus()
+ else
+ function frame:PerformLayout()
+ if lua_editor_previous_dimensions then
+ local x,y = frame:LocalToScreen()
+ lua_editor_previous_dimensions = {
+ x = x,
+ y = y,
+ wide = frame:GetWide(),
+ tall = frame:GetTall()
+ }
+ end
+ btn_calcdrag_remove:SetX(self:GetWide() - 180 + 4)
+ btn_remember_dimensions:SetX(self:GetWide() - 160 + 4)
+ btn_fontplus:SetX(self:GetWide() - 120 + 4)
+ btn_fontminus:SetX(self:GetWide() - 140 + 4)
+ perflayout(self)
+ end
+ end
+
+end
+
do -- script
local PANEL = {}
@@ -602,9 +830,450 @@ do -- script
frame:SetTitle(title)
end
+ add_fontsize_buttons(frame, editor)
+
+ pace.ActiveSpecialPanel = frame
+ end
+
+ pace.RegisterPanel(PANEL)
+end
+
+
+local function install_edge_resizes(frame)
+ local function more_or_less(n1,n2)
+ return math.abs(n1-n2) < 10
+ end
+ pac.AddHook("Think", frame, function()
+ local w,h = frame:GetSize()
+ local px,py = frame:GetPos()
+ local mx,my = input.GetCursorPos()
+ if not input.IsMouseDown(MOUSE_LEFT) then
+ frame.resizing = false
+ frame.resizing_down = false
+ frame.resizing_left = false
+ frame.resizing_right = false
+ elseif frame.resizing then
+ if pace.dragging then return end
+ if frame.resizing_down then
+ frame:SetHeight(math.max(my-py, 100))
+ elseif frame.resizing_left then
+ frame:SetWide(math.max(frame.target_edge - mx, 100))
+ frame:SetX(mx)
+ elseif frame.resizing_right then
+ frame:SetWide(mx-px)
+ end
+ end
+ if more_or_less(px+w,mx) then --EDGE RIGHT
+ frame:SetCursor("sizewe")
+ if input.IsMouseDown(MOUSE_LEFT) then
+ frame.resizing = true
+ frame.resizing_right = true
+ end
+ end
+ if more_or_less(px,mx) then --EDGE LEFT
+ frame:SetCursor("sizewe")
+ frame.target_edge = px+w
+ if input.IsMouseDown(MOUSE_LEFT) then
+ frame.resizing = true
+ frame.resizing_left = true
+ end
+ end
+ if more_or_less(py+h,my) then --EDGE DOWN
+ frame:SetCursor("sizens")
+ if input.IsMouseDown(MOUSE_LEFT) then
+ frame.resizing = true
+ frame.resizing_down = true
+ end
+ end
+ end)
+end
+
+do -- script command
+ local PANEL = {}
+
+ PANEL.ClassName = "properties_code_script"
+ PANEL.Base = "pace_properties_base_type"
+
+ function PANEL:SetValue(var, skip_encode)
+ if self.editing then return end
+
+ local value = skip_encode and var or self:Encode(var)
+ if isnumber(value) then
+ -- visually round numbers so 0.6 doesn't show up as 0.600000000001231231 on wear
+ value = math.Round(value, 7)
+ end
+ local str = tostring(value)
+ local original_str = string.Trim(str,"\n")
+ local lines = string.Explode("\n", original_str)
+ if #lines > 1 then
+ if lines[#lines] ~= "" then
+ str = ""
+ elseif #lines > 2 then
+ str = ""
+ end
+ end
+
+ self:SetTextColor(self.alt_line and self:GetSkin().Colours.Category.AltLine.Text or self:GetSkin().Colours.Category.Line.Text)
+ if str == "" then self:SetTextColor(Color(160,0,80)) end
+ self:SetFont(pace.CurrentFont)
+ self:SetText(" " .. str) -- ugh
+ self:SizeToContents()
+
+ if #str > 10 then
+ self:SetTooltip(value)
+ else
+ self:SetTooltip()
+ end
+
+ self.original_text = original_str
+ self.original_str = original_str
+ self.original_var = var
+
+ if self.OnValueSet then
+ self:OnValueSet(var)
+ end
+ end
+
+ function PANEL:MoreOptionsLeftClick()
+ local part = pace.current_part
+ pace.SafeRemoveSpecialPanel()
+
+ local frame = vgui.Create("DFrame")
+ install_edge_resizes(frame)
+ frame:SetTitle(L"script")
+ pace.ShowSpecial(frame, self, 0)
+ frame:SetSizable(true)
+
+ local editor = vgui.Create("pace_luapad", frame)
+ frame.luapad = editor
+ install_fontsize_buttons(frame, editor, true, self.CurrentKey)
+ editor:Dock(FILL)
+
+ if lua_editor_previous_dimensions ~= nil then
+ frame:SetPos(lua_editor_previous_dimensions.x,lua_editor_previous_dimensions.y)
+ frame:SetSize(lua_editor_previous_dimensions.wide,lua_editor_previous_dimensions.tall)
+ else
+ if pace.Editor:IsLeft() then
+ frame:SetSize(ScrW() - pace.Editor:GetX() - pace.Editor:GetWide(),200)
+ frame:SetPos(pace.Editor:GetWide() + pace.Editor:GetX(), select(2, self:LocalToScreen()))
+ else
+ frame:SetSize(pace.Editor:GetX(),200)
+ frame:SetPos(0, select(2, self:LocalToScreen()))
+ end
+ end
+
+ editor:SetText(part[self.CurrentKey])
+ local pnl = self
+ if pnl.CurrentKey == "String" then
+ editor.OnTextChanged = function(self)
+ local str = self:GetValue():Trim("\n")
+ if input.IsButtonDown(KEY_ENTER) then part:SetString(str) end
+ pnl:SetValue(str)
+ end
+ editor.OnRemove = function(self)
+ local str = self:GetValue():Trim("\n")
+ part:SetString(str)
+ end
+ elseif pnl.CurrentKey == "OnHideString" then
+ editor.OnTextChanged = function(self)
+ local str = self:GetValue():Trim("\n")
+ if input.IsButtonDown(KEY_ENTER) then part:SetOnHideString(str) end
+ pnl:SetValue(str)
+ end
+ editor.OnRemove = function(self)
+ local str = self:GetValue():Trim("\n")
+ part:SetOnHideString(str)
+ end
+ elseif pnl.CurrentKey == "DelayedString" then
+ editor.OnTextChanged = function(self)
+ local str = self:GetValue():Trim("\n")
+ if input.IsButtonDown(KEY_ENTER) then part:SetDelayedString(str) end
+ pnl:SetValue(str)
+ end
+ editor.OnRemove = function(self)
+ local str = self:GetValue():Trim("\n")
+ part:SetDelayedString(str)
+ end
+ end
+
+ editor.last_error = ""
+
+ function editor:CheckGlobal(str)
+ if not part:IsValid() then frame:Remove() return end
+
+ return part:ShouldHighlight(str)
+ end
+
+ function editor:Think()
+ if not part:IsValid() then frame:Remove() return end
+
+ local title = L"script editor"
+ if part.UseLua then
+ title = L"command" .. " (lua)"
+ else
+ title = L"command" .. " (console)"
+ end
+ if frame:GetTitle() == "successfully compiled" then
+ title = "(lua) successfully compiled"
+ end
+
+ if part.Error then
+ title = part.Error
+
+ local line = tonumber(title:match("SCRIPT_ENV:(%d-):"))
+
+ if line then
+ title = title:match("SCRIPT_ENV:(.+)")
+ if self.last_error ~= title then
+ editor:SetScrollPosition(line)
+ editor:SetErrorLine(line)
+ self.last_error = title
+ end
+ end
+ else
+ editor:SetErrorLine(nil)
+
+ if part.script_printing then
+ title = part.script_printing
+ part.script_printing = nil
+ end
+ end
+ title = frame.special_title or title
+ frame:SetTitle(title)
+ end
+
+
+
+ editor.FontWidth, editor.FontHeight = surface.GetTextSize(" ")
+
+ pace.ActiveSpecialPanel = frame
+ end
+
+ pace.RegisterPanel(PANEL)
+end
+
+do -- script proxy
+ local PANEL = {}
+
+ PANEL.ClassName = "properties_code_proxy"
+ PANEL.Base = "pace_properties_base_type"
+
+ function PANEL:SetValue(var, skip_encode)
+ if self.editing then return end
+
+ local value = skip_encode and var or self:Encode(var)
+ if isnumber(value) then
+ -- visually round numbers so 0.6 doesn't show up as 0.600000000001231231 on wear
+ value = math.Round(value, 7)
+ elseif isvector(var) then
+ var = math.Round(var.x,3) .. "," .. math.Round(var.y,3) .. "," .. math.Round(var.z,3)
+ value = var
+ elseif isangle(var) then
+ var = math.Round(var.p,3) .. "," .. math.Round(var.y,3) .. "," .. math.Round(var.r,3)
+ value = var
+ elseif IsColor(var) then
+ var = math.Round(var.r,3) .. "," .. math.Round(var.g,3) .. "," .. math.Round(var.b,3)
+ value = var
+ end
+ local str = tostring(value)
+ local original_str = string.Trim(str,"\n")
+ local lines = string.Explode("\n", str)
+ if #lines > 1 then
+ if lines[#lines] ~= "" then
+ str = ""
+ elseif #lines > 2 then
+ str = ""
+ end
+ end
+ str = string.gsub(str, "\n", "")
+ self:SetTextColor(self.alt_line and self:GetSkin().Colours.Category.AltLine.Text or self:GetSkin().Colours.Category.Line.Text)
+ if str == "" then self:SetTextColor(Color(160,0,80)) end
+ self:SetFont(pace.CurrentFont)
+ self:SetText(" " .. str) -- ugh
+ self:SizeToContents()
+
+ if #str > 10 then
+ self:SetTooltip(str)
+ else
+ self:SetTooltip()
+ end
+
+ self.original_text = original_str
+ self.original_str = original_str
+ self.original_var = var
+
+ if self.OnValueSet then
+ self:OnValueSet(var)
+ end
+ end
+
+ function PANEL:MoreOptionsLeftClick()
+ local key = self.CurrentKey
+ local part = pace.current_part
+ local prop = self
+ pace.SafeRemoveSpecialPanel()
+
+ local frame = vgui.Create("DFrame")
+ install_edge_resizes(frame)
+ frame:SetTitle(L"proxy")
+ pace.ShowSpecial(frame, self, 0)
+ frame:SetSizable(true)
+
+ local editor = vgui.Create("pace_luapad", frame)
+ local slots = {
+ ["ExpressionOnHide"] = 0,
+ ["Extra1"] = 1,
+ ["Extra2"] = 2,
+ ["Extra3"] = 3,
+ ["Extra4"] = 4,
+ ["Extra5"] = 5,
+ }
+ editor.keynumber = slots[self.CurrentKey]
+ frame.luapad = editor
+ install_fontsize_buttons(frame, editor)
+ editor:Dock(FILL)
+ if lua_editor_previous_dimensions ~= nil then
+ frame:SetPos(lua_editor_previous_dimensions.x,lua_editor_previous_dimensions.y)
+ frame:SetSize(lua_editor_previous_dimensions.wide,lua_editor_previous_dimensions.tall)
+ else
+ if pace.Editor:IsLeft() then
+ frame:SetSize(ScrW() - pace.Editor:GetX() - pace.Editor:GetWide(),200)
+ frame:SetPos(pace.Editor:GetWide() + pace.Editor:GetX(), select(2, self:LocalToScreen()))
+ else
+ frame:SetSize(pace.Editor:GetX(),200)
+ frame:SetPos(0, select(2, self:LocalToScreen()))
+ end
+ end
+
+ editor:SetText(part["Get"..key](part))
+ editor.OnTextChanged = function(self)
+ part["Set"..key](part, self:GetValue())
+ prop:SetValue(self:GetValue())
+ end
+
+ editor.last_error = ""
+
+ function editor:ShouldHighlight(str)
+ return part.lib and part.lib[str]
+ end
+
+ function editor:CheckGlobal(str)
+ if not part:IsValid() then frame:Remove() return end
+
+ return self:ShouldHighlight(str)
+ end
+
+ function editor:Think()
+ if not part:IsValid() then frame:Remove() return end
+
+ local title = L"proxy editor"
+
+ if part.Error then
+ title = part.Error
+
+ local line = tonumber(title:match("SCRIPT_ENV:(%d-):"))
+
+ if line then
+ title = title:match("SCRIPT_ENV:(.+)")
+ if self.last_error ~= title then
+ editor:SetScrollPosition(line)
+ editor:SetErrorLine(line)
+ self.last_error = title
+ end
+ end
+ else
+ editor:SetErrorLine(nil)
+
+ if part.script_printing then
+ title = part.script_printing
+ part.script_printing = nil
+ end
+ end
+ title = frame.special_title or title
+ frame:SetTitle(title)
+ end
+
pace.ActiveSpecialPanel = frame
end
+ function PANEL:EditText()
+ local oldText = self:GetText()
+ self:SetText("")
+
+ local pnl = vgui.Create("DTextEntry")
+ self.editing = pnl
+ pnl:SetFont(pace.CurrentFont)
+ pnl:SetDrawBackground(false)
+ pnl:SetDrawBorder(false)
+ local enc = self:EncodeEdit(self.original_str or "")
+ if enc == "" then
+ enc = self.original_str
+ end
+ pnl:SetText(enc)
+ pnl:SetKeyboardInputEnabled(true)
+ pnl:RequestFocus()
+ pnl:SelectAllOnFocus(true)
+
+ pnl.OnTextChanged = function() oldText = pnl:GetText() end
+
+ local hookID = tostring({})
+ local textEntry = pnl
+ local delay = os.clock() + 0.5
+
+ pac.AddHook('Think', hookID, function(code)
+ if not IsValid(self) or not IsValid(textEntry) then return pac.RemoveHook('Think', hookID) end
+ if textEntry:IsHovered() or self:IsHovered() then return end
+ if delay > os.clock() then return end
+ if not input.IsMouseDown(MOUSE_LEFT) and not input.IsKeyDown(KEY_ESCAPE) then return end
+ pac.RemoveHook('Think', hookID)
+ self.editing = false
+ pace.BusyWithProperties = NULL
+ textEntry:Remove()
+ self:SetText(oldText)
+ pnl:OnEnter()
+ end)
+
+ --local x,y = pnl:GetPos()
+ --pnl:SetPos(x+3,y-4)
+ --pnl:Dock(FILL)
+ local x, y = self:LocalToScreen()
+ local inset_x = self:GetTextInset()
+ pnl:SetPos(x+5 + inset_x, y)
+ pnl:SetSize(self:GetSize())
+ pnl:SetWide(ScrW())
+ pnl:MakePopup()
+
+ pnl.OnEnter = function()
+ pace.BusyWithProperties = NULL
+ self.editing = false
+
+ pnl:Remove()
+
+ self:SetText(tostring(self:Encode(self:DecodeEdit(pnl:GetText() or ""))), true)
+ self.OnValueChanged(self:Decode(self:GetText()))
+ end
+
+ local old = pnl.Paint
+ pnl.Paint = function(...)
+ if not self:IsValid() then pnl:Remove() return end
+ local x, y = self:LocalToScreen()
+ y = math.Clamp(y,0,ScrH() - self:GetTall())
+ pnl:SetPos(x + 5 + inset_x, y)
+ surface.SetFont(pnl:GetFont())
+ local w = surface.GetTextSize(pnl:GetText()) + 6
+
+ surface.DrawRect(0, 0, w, pnl:GetTall())
+ surface.SetDrawColor(self:GetSkin().Colours.Properties.Border)
+ surface.DrawOutlinedRect(0, 0, w, pnl:GetTall())
+
+ pnl:SetWide(w)
+
+ old(...)
+ end
+
+ pace.BusyWithProperties = pnl
+ end
+
pace.RegisterPanel(PANEL)
end
@@ -714,9 +1383,16 @@ do -- event is_touching
if part ~= last_part then stop() return end
if not part:IsValid() then stop() return end
if part.ClassName ~= "event" then stop() return end
- if part:GetEvent() ~= "is_touching" then stop() return end
+ if not (part:GetEvent() == "is_touching" or part:GetEvent() == "is_touching_scalable" or part:GetEvent() == "is_touching_filter" or part:GetEvent() == "is_touching_life") then stop() return end
local extra_radius = part:GetProperty("extra_radius") or 0
+ local nearest_model = part:GetProperty("nearest_model") or false
+ local no_npc = part:GetProperty("no_npc") or false
+ local no_players = part:GetProperty("no_players") or false
+ local x_stretch = part:GetProperty("x_stretch") or 1
+ local y_stretch = part:GetProperty("y_stretch") or 1
+ local z_stretch = part:GetProperty("z_stretch") or 1
+
local ent
if part.RootOwner then
ent = part:GetRootPart():GetOwner()
@@ -724,34 +1400,223 @@ do -- event is_touching
ent = part:GetOwner()
end
+ if nearest_model then ent = part:GetOwner() end
+
if not IsValid(ent) then stop() return end
- local radius = ent:BoundingRadius()
+ local radius
if radius == 0 and IsValid(ent.pac_projectile) then
radius = ent.pac_projectile:GetRadius()
end
- radius = math.max(radius + extra_radius + 1, 1)
+ local mins = Vector(-x_stretch,-y_stretch,-z_stretch)
+ local maxs = Vector(x_stretch,y_stretch,z_stretch)
- local mins = Vector(-1,-1,-1)
- local maxs = Vector(1,1,1)
- local startpos = ent:WorldSpaceCenter()
+ radius = math.max(ent:BoundingRadius() + extra_radius + 1, 1)
mins = mins * radius
maxs = maxs * radius
- local tr = util.TraceHull( {
- start = startpos,
- endpos = startpos,
- maxs = maxs,
- mins = mins,
- filter = ent
- } )
+ local startpos = ent:WorldSpaceCenter()
+ local b = false
+ if part:GetEvent() == "is_touching" then
+ local tr = util.TraceHull( {
+ start = startpos,
+ endpos = startpos,
+ maxs = maxs,
+ mins = mins,
+ filter = {part:GetRootPart():GetOwner(),ent}
+ } )
+ b = tr.Hit
+ elseif part:GetEvent() == "is_touching_scalable" then
+ radius = math.max(extra_radius, 1) --oops, extra_radius is not accounted. but we need to keep backward compatibility
+ mins = Vector(-x_stretch,-y_stretch,-z_stretch)
+ maxs = Vector(x_stretch,y_stretch,z_stretch)
+ mins = mins * radius
+ maxs = maxs * radius
+ if part:GetProperty("world_only") then
+ local tr = util.TraceHull( {
+ start = startpos,
+ endpos = startpos,
+ maxs = maxs,
+ mins = mins,
+ filter = function(ent) return ent:IsWorld() end
+ } )
+ b = tr.Hit
+ else
+ local tr = util.TraceHull( {
+ start = startpos,
+ endpos = startpos,
+ maxs = maxs,
+ mins = mins,
+ filter = {part:GetRootPart():GetOwner(),ent}
+ } )
+ b = tr.Hit
+ end
+ elseif part:GetEvent() == "is_touching_life" then
+ local found = false
+ local ents_hits = ents.FindInBox(startpos + mins, startpos + maxs)
+ for _,ent2 in pairs(ents_hits) do
+
+ if IsValid(ent2) and (ent2 ~= ent and ent2 ~= part:GetRootPart():GetOwner()) and
+ (ent2:IsNPC() or ent2:IsPlayer())
+ then
+ found = true
+ if ent2:IsNPC() and no_npc then
+ found = false
+ elseif ent2:IsPlayer() and no_players then
+ found = false
+ end
+ if found then b = true end
+ end
+ end
+ elseif part:GetEvent() == "is_touching_filter" then
+ local ents_hits = ents.FindInBox(startpos + mins, startpos + maxs)
+ for _,ent2 in pairs(ents_hits) do
+ if (ent2 ~= ent and ent2 ~= part:GetRootPart():GetOwner()) and
+ (ent2:IsNPC() or ent2:IsPlayer()) and
+ not ( (no_npc and ent2:IsNPC()) or (no_players and ent2:IsPlayer()) )
+ then b = true end
+ end
+ end
+
+ if self.udata then
+ render.DrawWireframeBox( startpos, Angle( 0, 0, 0 ), mins, maxs, b and Color(255,0,0) or Color(255,255,255), true )
+ end
+ end)
+ end
+
+ pace.RegisterPanel(PANEL)
+end
+
+do -- event seen_by_player
+ local PANEL = {}
+
+ PANEL.ClassName = "properties_seen_by_player"
+ PANEL.Base = "pace_properties_number"
+
+ function PANEL:OnValueSet()
+ local function stop()
+ hook.Remove("PostDrawOpaqueRenderables", "pace_draw_is_touching2")
+ end
+ local last_part = pace.current_part
+
+ hook.Add("PostDrawOpaqueRenderables", "pace_draw_is_touching2", function()
+ local part = pace.current_part
+ if part ~= last_part then stop() return end
+ if not part:IsValid() then stop() return end
+ if part.ClassName ~= "event" then stop() return end
+ if not (part:GetEvent() == "seen_by_player") then stop() return end
+ if not pace.IsActive() then stop() return end
+
+ local extra_radius = part:GetProperty("extra_radius") or 0
+
+ local ent
+ if part.RootOwner then
+ ent = part:GetRootPart():GetOwner()
+ else
+ ent = part:GetOwner()
+ end
+
+ if not IsValid(ent) then stop() return end
+
+ local mins = Vector(-extra_radius,-extra_radius,-extra_radius)
+ local maxs = Vector(extra_radius,extra_radius,extra_radius)
+ local b = false
+ local players_see = {}
+ for _,v in ipairs(player.GetAll()) do
+ if v == ent then continue end
+ local eyetrace = v:GetEyeTrace()
+
+ local this_player_sees = false
+ if util.IntersectRayWithOBB(eyetrace.StartPos, eyetrace.HitPos - eyetrace.StartPos, LocalPlayer():GetPos() + LocalPlayer():OBBCenter(), Angle(0,0,0), Vector(-extra_radius,-extra_radius,-extra_radius), Vector(extra_radius,extra_radius,extra_radius)) then
+ b = true
+ this_player_sees = true
+ end
+ if eyetrace.Entity == ent then
+ b = true
+ this_player_sees = true
+ end
+ render.DrawLine(eyetrace.StartPos, eyetrace.HitPos, this_player_sees and Color(255, 0,0) or Color(255,255,255), true)
+ end
+ ::CHECKOUT::
if self.udata then
- render.DrawWireframeBox( startpos, Angle( 0, 0, 0 ), mins, maxs, tr.Hit and Color(255,0,0) or Color(255,255,255), true )
+ render.DrawWireframeBox( ent:GetPos() + ent:OBBCenter(), Angle( 0, 0, 0 ), mins, maxs, b and Color(255,0,0) or Color(255,255,255), true )
end
end)
end
pace.RegisterPanel(PANEL)
end
+
+do --projectile radius
+ local PANEL = {}
+
+ PANEL.ClassName = "properties_projectile_radii"
+ PANEL.Base = "pace_properties_number"
+
+ local testing_mesh
+ local drawing = false
+ local phys_mesh_vis = {}
+ function PANEL:OnValueSet()
+ time = os.clock() + 6
+ local function stop()
+ hook.Remove("PostDrawOpaqueRenderables", "pace_draw_projectile_radii")
+ timer.Simple(0.2, function() SafeRemoveEntity(testing_mesh) end) drawing = false
+ end
+ local last_part = pace.current_part
+
+ hook.Add("PostDrawOpaqueRenderables", "pace_draw_projectile_radii", function()
+ drawing = true
+ if time < os.clock() then
+ stop()
+ end
+ if not pace.current_part:IsValid() then stop() return end
+ if pace.current_part.ClassName ~= "projectile" then stop() return end
+ if self.udata then
+ if last_part.Sphere then
+ render.DrawWireframeSphere( last_part:GetWorldPosition(), last_part.Radius, 10, 10, Color(255,255,255), true )
+ render.DrawWireframeSphere( last_part:GetWorldPosition(), last_part.DamageRadius, 10, 10, Color(255,0,0), true )
+ else
+ local mins_ph = Vector(last_part.Radius,last_part.Radius,last_part.Radius)
+ local mins_dm = Vector(last_part.DamageRadius,last_part.DamageRadius,last_part.DamageRadius)
+ if last_part.OverridePhysMesh then
+ if not IsValid(testing_mesh) then testing_mesh = ents.CreateClientProp("models/props_junk/PopCan01a.mdl") end
+ testing_mesh:PhysicsInit(SOLID_VPHYSICS)
+ if testing_mesh:GetModel() ~= last_part.FallbackSurfpropModel then
+ testing_mesh:SetModel(last_part.FallbackSurfpropModel)
+ testing_mesh:PhysicsInit(SOLID_VPHYSICS)
+ phys_mesh_vis = {}
+ for i = 0, testing_mesh:GetPhysicsObjectCount() - 1 do
+ for i2,tri in ipairs(testing_mesh:GetPhysicsObjectNum( i ):GetMeshConvexes()) do
+ for i3,vert in ipairs(tri) do
+ table.insert(phys_mesh_vis, vert)
+ end
+ end
+ end
+ end
+ local obj = Mesh()
+ obj:BuildFromTriangles(phys_mesh_vis)
+ cam.Start3D(pac.EyePos, pac.EyeAng)
+ render.SetMaterial(Material("models/wireframe"))
+ local mat = Matrix()
+ mat:Translate(last_part:GetWorldPosition())
+ mat:Rotate(last_part:GetWorldAngles())
+ mat:Scale(Vector(last_part.Radius,last_part.Radius,last_part.Radius))
+ cam.PushModelMatrix( mat )
+ render.CullMode(MATERIAL_CULLMODE_CW)obj:Draw()
+ render.CullMode(MATERIAL_CULLMODE_CCW)obj:Draw()
+ cam.PopModelMatrix()
+ cam.End3D()
+ else
+ render.DrawWireframeBox( last_part:GetWorldPosition(), last_part:GetWorldAngles(), -mins_ph, mins_ph, Color(255,255,255), true )
+ end
+ render.DrawWireframeBox( last_part:GetWorldPosition(), last_part:GetWorldAngles(), -mins_dm, mins_dm, Color(255,0,0), true )
+ end
+
+ end
+ end)
+ end
+
+ pace.RegisterPanel(PANEL)
+end
\ No newline at end of file
diff --git a/lua/pac3/editor/client/panels/pac_tree.lua b/lua/pac3/editor/client/panels/pac_tree.lua
index 5389e0756..7462b7d26 100644
--- a/lua/pac3/editor/client/panels/pac_tree.lua
+++ b/lua/pac3/editor/client/panels/pac_tree.lua
@@ -30,7 +30,7 @@ AccessorFunc(PANEL, "m_bClickOnDragHover", "ClickOnDragHover")
function PANEL:Init()
self:SetShowIcons(true)
self:SetIndentSize(14)
- self:SetLineHeight(17)
+ self:SetLineHeight(17 * GetConVar("pac_editor_scale"):GetFloat())
self.RootNode = self:GetCanvas():Add("pac_dtree_node")
self.RootNode:SetRoot(self)
@@ -143,7 +143,10 @@ function PANEL:Init()
self.Label = vgui.Create("pac_dtree_node_button", self)
self.Label:SetDragParent(self)
self.Label.DoClick = function() self:InternalDoClick() end
- self.Label.DoDoubleClick = function() self:InternalDoClick() end
+ self.Label.DoDoubleClick = function()
+ self:InternalDoClick()
+ self.part:DoDoubleClick()
+ end
self.Label.DoRightClick = function() self:InternalDoRightClick() end
self.Label.DragHover = function(s, t) self:DragHover(t) end
diff --git a/lua/pac3/editor/client/panels/properties.lua b/lua/pac3/editor/client/panels/properties.lua
index b062fdd96..c57489c1a 100644
--- a/lua/pac3/editor/client/panels/properties.lua
+++ b/lua/pac3/editor/client/panels/properties.lua
@@ -1,6 +1,29 @@
local L = pace.LanguageString
local languageID = CreateClientConVar("pac_editor_languageid", 1, true, false, "Whether we should show the language indicator inside of editable text entries.")
+local favorites_menu_expansion = CreateClientConVar("pac_favorites_try_to_build_asset_series", "0", true, false)
+local extra_dynamic = CreateClientConVar("pac_special_property_update_dynamically", "1", true, false, "Whether proxies should refresh the properties, and some booleans may show more information.")
+local special_property_text_color = CreateClientConVar("pac_special_property_text_color", "160 0 80", true, false, "R G B color of special property text\npac_special_property_text_color \"\" will make it not change the color\nSpecial contexts like proxies and hidden parts can show a different color to show that changes are happening in real time.")
+
+pace.special_property_text_color = Color(160,0,80)
+if special_property_text_color:GetString() ~= "" then
+ local r,g,b = unpack(string.Split(special_property_text_color:GetString(), " "))
+ r = tonumber(r) or 0 g = tonumber(g) or 0 b = tonumber(b) or 0
+ pace.special_property_text_color = Color(r,g,b)
+else
+ pace.special_property_text_color = nil
+end
+cvars.AddChangeCallback("pac_special_property_text_color", function(cvar, old, new)
+ if new ~= "" then
+ local r,g,b = unpack(string.Split(special_property_text_color:GetString(), " "))
+ r = tonumber(r) or 0 g = tonumber(g) or 0 b = tonumber(b) or 0
+ pace.special_property_text_color = Color(r,g,b)
+ else
+ pace.special_property_text_color = nil
+ end
+end, "pac_change_special_property_text_color")
+
+local searched_cache_series_results = {}
function pace.ShowSpecial(pnl, parent, size)
size = size or 150
@@ -16,12 +39,203 @@ function pace.FixMenu(menu)
menu:SetPos(pace.Editor:GetPos() + pace.Editor:GetWide(), gui.MouseY() - (menu:GetTall() * 0.5))
end
+
+function pace.GoToPart(part)
+ pace.OnPartSelected(part, true)
+ local delay = 0
+ if not IsValid(part.pace_tree_node) then --possible de-loaded node
+ delay = 0.5
+ end
+ local parent = part:GetParent()
+ while IsValid(parent) and (parent:GetParent() ~= parent) do
+ parent:SetEditorExpand(true)
+ parent = parent:GetParent()
+ if parent:IsValid() then
+ parent:SetEditorExpand(true)
+ end
+ end
+ pace.RefreshTree(true)
+
+ timer.Simple(delay, function() if IsValid(part.pace_tree_node) then
+ pace.tree:ScrollToChild(part.pace_tree_node)
+ end end)
+end
+---returns table
+--start_index is the first known index
+--continuous is whether it's continuous (some series have holes)
+--end_index is the last known
+function pace.FindAssetSeriesBounds(base_directory, base_file, extension)
+
+ --LEADING ZEROES FIX NOT YET IMPLEMENTED
+ local function leading_zeros(str)
+ str = string.StripExtension(str)
+
+ local untilzero_pattern = "%f[1-9][0-9]+$"
+ local afterzero_pattern = "0+%f[1-9+]"
+ local beforenumbers_pattern = "%f[%f[1-9][0-9]+$]"
+ --string.gsub(str, "%f[1-9][0-9]+$", "") --get the start until the zeros stop
+
+ --string.gsub(str, "0+%f[1-9+]", "") --leave start
+
+ if string.find(str, afterzero_pattern) then
+ return string.gsub(str, untilzero_pattern, string.match(str, afterzero_pattern))
+ end
+ end
+ --print(base_file .. "leading zeros?" , leading_zeros(base_file))
+ if searched_cache_series_results[base_directory .. "/" .. base_file] then return searched_cache_series_results[base_directory .. "/" .. base_file] end
+ local tbl = {}
+ local i = 0 --try with 0 at first
+ local keep_looking = true
+ local file_n
+ local lookaheads_left = 15
+ local next_exists
+ tbl.start_index = nil
+ tbl.all_paths = {}
+ local index_compressed = 1 --increasing ID number of valid files
+
+ while keep_looking do
+
+ file_n = base_directory .. "/" .. base_file .. i .. "." .. extension
+ --print(file_n , "file" , file.Exists(file_n, "GAME") and "exists" or "doesn't exist")
+ --print("checking" , file_n) print("\tThe file" , file.Exists(file_n, "GAME") and "exists" or "doesn't exist")
+ if file.Exists(file_n, "GAME") then
+ if not tbl.start_index then tbl.start_index = i end
+ tbl.end_index = i
+ tbl.all_paths[index_compressed] = file_n
+ index_compressed = index_compressed + 1
+ end
+
+
+ i = i + 1
+ file_n = base_directory .. "/" .. base_file .. i .. "." .. extension
+ next_exists = file.Exists(file_n, "GAME")
+ if not next_exists then
+ if tbl.start_index then tbl.continuous = false end
+ lookaheads_left = lookaheads_left - 1
+ else
+ lookaheads_left = 15
+ end
+ keep_looking = next_exists or lookaheads_left > 0
+ end
+ if not tbl.start_index then tbl.continuous = false end
+ --print("result of search:")
+ --PrintTable(tbl)
+ searched_cache_series_results[base_directory .. "/" .. base_file] = tbl
+ return tbl
+end
+
+
+function pace.AddSubmenuWithBracketExpansion(pnl, func, base_file, extension, base_directory)
+ if extension == "vmt" then base_directory = "materials" end --prescribed format: short
+ if extension == "mdl" then base_directory = "models" end --prescribed format: full
+ if extension == "wav" or extension == "mp3" or extension == "ogg" then base_directory = "sound" end --prescribed format: no trunk
+
+ local base_file_original = base_file
+ if string.find(base_file, "%[%d+,%d+%]") then --find the bracket notation
+ base_file = string.gsub(base_file, "%[%d+,%d+%]$", "")
+ elseif string.find(base_file, "%d+") then
+ base_file = string.gsub(base_file, "%d+$", "")
+ end
+
+ local tbl = pace.FindAssetSeriesBounds(base_directory, base_file, extension)
+
+ local icon = "icon16/sound.png"
+
+ if string.find(base_file, "music") or string.find(base_file, "theme") then
+ icon = "icon16/music.png"
+ elseif string.find(base_file, "loop") then
+ icon = "icon16/arrow_rotate_clockwise.png"
+ end
+
+ if base_directory == "materials" then
+ icon = "icon16/paint_can.png"
+ elseif base_directory == "models" then
+ icon = "materials/spawnicons/"..string.gsub(base_file, ".mdl", "")..".png"
+ end
+
+ local pnl2
+ local menu2
+ --print(base_file , #tbl.all_paths)
+ if #tbl.all_paths > 1 then
+ pnl2, menu2 = pnl:AddSubMenu(base_file .. " series", function()
+ func(base_file_original .. "." .. extension)
+ end)
+
+ if base_directory == "materials" then
+ menu2:SetImage("icon16/table_multiple.png")
+ --local mat = string.gsub(base_file_original, "." .. string.GetExtensionFromFilename(base_file_original), "")
+ --pnl2:AddOption(mat, function() func(base_file_original) end):SetImage("icon16/paint_can.png")
+ elseif base_directory == "models" then
+ menu2:SetImage(icon)
+ elseif base_directory == "sound" then
+ --print("\t" .. icon)
+ menu2:SetImage(icon)
+ end
+
+ else
+ if base_directory == "materials" then
+ --local mat = string.gsub(base_file_original, "." .. string.GetExtensionFromFilename(base_file_original), "")
+ --pnl2:AddOption(mat, function() func(base_file_original) end):SetImage("icon16/paint_can.png")
+ elseif base_directory == "models" then
+
+ elseif base_directory == "sound" then
+ local snd = base_file_original
+ menu2 = pnl:AddOption(snd, function() func(snd) end):SetImage(icon)
+ end
+ end
+
+
+ --print(tbl)
+ --PrintTable(tbl.all_paths)
+ if not tbl then return end
+ if #tbl.all_paths > 1 then
+ for _,path in ipairs(tbl.all_paths) do
+ path_no_trunk = string.gsub(path, base_directory .. "/", "")
+ if base_directory == "materials" then
+ local mat = string.gsub(path_no_trunk, "." .. string.GetExtensionFromFilename(path_no_trunk), "")
+ pnl2:AddOption(mat, function() func(mat) end):SetMaterial(pace.get_unlit_mat(path))
+
+ elseif base_directory == "models" then
+ local mdl = path
+ pnl2:AddOption(string.GetFileFromFilename(mdl), function() func(mdl) end):SetImage("materials/spawnicons/"..string.gsub(mdl, ".mdl", "")..".png")
+
+ elseif base_directory == "sound" then
+ local snd = path_no_trunk
+ pnl2:AddOption(snd, function() func(snd) end):SetImage(icon)
+ end
+ end
+ end
+
+
+
+end
+
+local function get_files_recursively(tbl, path, extension)
+ local returning_call = false
+ if not tbl then returning_call = true tbl = {} end
+ local files, folders = file.Find(path .. "/*", "GAME")
+ for _,file in ipairs(files) do
+ if istable(extension) then
+ for _,ext in ipairs(extension) do
+ if string.GetExtensionFromFilename(file) == ext then
+ table.insert(tbl, path.."/"..file)
+ end
+ end
+ elseif string.GetExtensionFromFilename(file) == extension then
+ table.insert(tbl, path.."/"..file)
+ end
+ end
+ for _,folder in ipairs(folders) do get_files_recursively(tbl, path.."/"..folder, extension) end
+ if returning_call then return tbl end
+end
+
local function DefineMoreOptionsLeftClick(self, callFuncLeft, callFuncRight)
local btn = vgui.Create("DButton", self)
btn:SetSize(16, 16)
btn:Dock(RIGHT)
btn:SetText("...")
btn.DoClick = function() callFuncLeft(self, self.CurrentKey) end
+ btn.PerformLayout = function() btn:SetWide(self:GetTall()) end
if callFuncRight then
btn.DoRightClick = function() callFuncRight(self, self.CurrentKey) end
@@ -89,6 +303,12 @@ function pace.CreateSearchList(property, key, name, add_columns, get_list, get_c
pnl.list_key = key
pnl.list_val = val
+ if name == "Input" or name == "Function" then --insert proxy function tutorials as tooltips
+ pnl:SetTooltip(pace.TUTORIALS["proxy_functions"][key] or "")
+ elseif name == "Event" then --insert event tutorials as tooltips
+ pnl:SetTooltip(pace.TUTORIALS["events"][key])
+ end
+
if not first:IsValid() then
first = pnl
end
@@ -126,6 +346,7 @@ end
pac.AddHook("GUIMousePressed", "pace_SafeRemoveSpecialPanel", function()
local pnl = pace.ActiveSpecialPanel
if pnl:IsValid() then
+ if pnl.ignore_saferemovespecialpanel then return end
local x,y = input.GetCursorPos()
local _x, _y = pnl:GetPos()
if x < _x or y < _y or x > _x + pnl:GetWide() or y > _y + pnl:GetTall() then
@@ -134,6 +355,25 @@ pac.AddHook("GUIMousePressed", "pace_SafeRemoveSpecialPanel", function()
end
end)
+pac.AddHook("PostRenderVGUI", "flash_properties", function()
+ if not pace.flashes then return end
+ for pnl, tbl in pairs(pace.flashes) do
+ if IsValid(pnl) then
+ --print(pnl:LocalToScreen(0,0))
+ local x,y = pnl:LocalToScreen(0,0)
+ local flash_alpha = 255*math.pow(math.Clamp((tbl.flash_end - CurTime()) / 2.5,0,1), 0.6)
+ surface.SetDrawColor(Color(tbl.color.r, tbl.color.g, tbl.color.b, flash_alpha))
+ local flash_size = 300*math.pow(math.Clamp((tbl.flash_end - 1.8 - CurTime()) / 0.7,0,1), 8) + 5
+ if pnl:GetY() > 4 then
+ surface.DrawOutlinedRect(-flash_size + x,-flash_size + y,pnl:GetWide() + 2*flash_size,pnl:GetTall() + 2*flash_size,5)
+ surface.SetDrawColor(Color(tbl.color.r, tbl.color.g, tbl.color.b, flash_alpha/2))
+ surface.DrawOutlinedRect(-flash_size + x - 3,-flash_size + y - 3,pnl:GetWide() + 2*flash_size + 6,pnl:GetTall() + 2*flash_size + 6,2)
+ end
+ if tbl.flash_end < CurTime() then pace.flashes[pnl] = nil end
+ end
+ end
+end)
+
do -- container
local PANEL = {}
@@ -159,6 +399,31 @@ do -- container
derma.SkinHook( "Paint", "CategoryButton", self, w, h )
end
+ function PANEL:Flash()
+ if not IsValid(pace.tree) or not IsValid(pace.properties) then return end
+ pace.flashes = pace.flashes or {}
+ pace.flashes[self] = {start = CurTime(), flash_end = CurTime() + 2.5, color = Color(255,0,0)}
+
+ do --scroll to the property
+ local _,y = self:LocalToScreen(0,0)
+ local _,py = pace.properties:LocalToScreen(0,0)
+ local scry = pace.properties.scr:GetScroll()
+
+ if y > ScrH() then
+ pace.properties.scr:SetScroll(scry - py + y)
+ elseif y < py - 200 then
+ pace.properties.scr:SetScroll(scry + (y - py) - 100)
+ end
+ end
+
+ do --scroll to the tree node
+ if self:GetChildren()[1].part.pace_tree_node then
+ pace.tree:ScrollToChild(self:GetChildren()[1].part.pace_tree_node)
+ end
+ end
+
+ end
+
function PANEL:SetContent(pnl)
pnl:SetParent(self)
self.content = pnl
@@ -172,6 +437,26 @@ do -- container
end
end
+ function PANEL:CreateAlternateLabel(str, no_offset)
+ if not str then
+ if self.alt_label then
+ if IsValid(self.alt_label) then
+ self.alt_label:Remove()
+ end
+ end
+ return
+ end
+ if str == "" then return end
+ self.alt_label = vgui.Create("DLabel", self)
+ self.alt_label:SetText("<" .. L(str) .. ">")
+ if pace.special_property_text_color then self.alt_label:SetTextColor(pace.special_property_text_color)
+ else self.alt_label:SetTextColor(self.alt_line and self:GetSkin().Colours.Category.AltLine.Text or self:GetSkin().Colours.Category.Line.Text) end
+ self.alt_label:SetPos(no_offset and 0 or 60,-1)
+ self.alt_label:SetSize(200,20)
+ self.alt_label:SetFont(pace.CurrentFont)
+ return self.alt_label
+ end
+
pace.RegisterPanel(PANEL)
end
@@ -393,6 +678,10 @@ do -- list
local btn = pace.CreatePanel("properties_label")
btn:SetTall(self:GetItemHeight())
+ --description tooltips should be on the text label. they are broken on every type except boolean.
+ if udata and udata.description then
+ btn:SetTooltip(udata.description)
+ end
do
local key = key
if key:EndsWith("UID") then
@@ -400,8 +689,16 @@ do -- list
end
btn:SetValue(L((udata and udata.editor_friendly or key):gsub("%u", " %1"):lower()):Trim())
+ pace.current_part["pac_property_label_"..key] = btn
+ if udata then
+ if udata.group == "bodygroups" then
+ if key[1] == "_" then --bodygroup exceptions
+ btn.lbl:SetText(key:sub(2,-1))
+ end
+ end
+ end
end
-
+
if obj then
btn.key_name = key
btn.part_namepart_name = obj.ClassName
@@ -417,6 +714,28 @@ do -- list
if ispanel(var) then
pnl:SetContent(var)
+ pace.current_part["pac_property_panel_"..key] = var
+
+ if key == "Hide" then
+ local reasons_hidden = pace.current_part:GetReasonsHidden()
+ if not table.IsEmpty(reasons_hidden) then
+ pnl:SetTooltip("Hidden by:" .. table.ToString(reasons_hidden, "", true))
+ local label = pnl:CreateAlternateLabel("hidden")
+ label.DoRightClick = function()
+ local menu = DermaMenu()
+ menu:SetPos(input.GetCursorPos())
+ for part,reason in pairs(tbl) do
+ if part ~= pace.current_part then
+ menu:AddOption("jump to " .. tostring(part), function()
+ pace.GoToPart(part)
+ end):SetImage("icon16/arrow_turn_right.png")
+ end
+ end
+ menu:MakePopup()
+ end
+ end
+ pace.current_part.hide_property_pnl = var
+ end
end
self.left:AddItem(btn)
@@ -449,6 +768,7 @@ do -- list
end
function PANEL:Clear()
+ if pace.bypass_tree then return end
for key, data in pairs(self.List) do
data.left:Remove()
data.right:Remove()
@@ -502,6 +822,7 @@ do -- list
end
function PANEL:Populate(flat_list)
+ if pace.bypass_tree then return end
self:Clear()
for _, data in ipairs(SortGroups(FlatListToGroups(flat_list))) do
@@ -518,7 +839,11 @@ do -- list
if prop.udata and prop.udata.editor_panel then
T = prop.udata.editor_panel or T
elseif pace.PanelExists("properties_" .. prop.key:lower()) then
- T = prop.key:lower()
+ --is it code bloat to fix weird edge cases like bodygroups on specific models???
+ --idk but it's more egregious to allow errors just because of what bodygroups the model has
+ if prop.key:lower() ~= "container" then
+ T = prop.key:lower()
+ end
elseif not pace.PanelExists("properties_" .. T) then
T = "string"
end
@@ -723,14 +1048,102 @@ do -- non editable string
pace.RegisterPanel(PANEL)
end
+local position_multicopy_properties = {
+ ["Position"] = true,
+ ["Angles"] = true,
+ ["PositionOffset"] = true,
+ ["AngleOffset"] = true,
+}
+local appearance_multicopy_properties = {
+ ["Material"] = true,
+ ["Color"] = true,
+ ["Brightness"] = true,
+ ["Alpha"] = true,
+ ["Translucent"] = true,
+ ["BlendMode"] = true
+}
+
+local function install_movable_multicopy(copymenu, key)
+ if position_multicopy_properties[key] then
+ copymenu:AddOption("Copy Angles & Position", function()
+ pace.MultiCopy(pace.current_part, {"Angles", "Position"})
+ end)
+ copymenu:AddOption("Copy Angle & Position Offsets", function()
+ pace.MultiCopy(pace.current_part, {"AngleOffset", "PositionOffset"})
+ end)
+ copymenu:AddOption("Copy Angle & Position and their Offsets", function()
+ pace.MultiCopy(pace.current_part, {"Angles", "Position", "AngleOffset", "PositionOffset"})
+ end)
+ copymenu:AddOption("Copy Angles & Angle Offset", function()
+ pace.MultiCopy(pace.current_part, {"Angles", "AngleOffset"})
+ end)
+ copymenu:AddOption("Copy Position & Position Offset", function()
+ pace.MultiCopy(pace.current_part, {"Position", "PositionOffset"})
+ end)
+ end
+end
+local function install_appearance_multicopy(copymenu, key)
+ if appearance_multicopy_properties[key] then
+ copymenu:AddOption("Material & Color", function()
+ pace.MultiCopy(pace.current_part, {"Material", "Color"})
+ end)
+ copymenu:AddOption("Material & Color & Brightness", function()
+ pace.MultiCopy(pace.current_part, {"Material", "Color", "Brightness"})
+ end)
+ copymenu:AddOption("Transparency", function()
+ pace.MultiCopy(pace.current_part, {"Alpha", "Translucent", "BlendMode"})
+ end):SetTooltip("Alpha, Translucent, Blend mode")
+ copymenu:AddOption("Copy Material & Color & Alpha", function()
+ pace.MultiCopy(pace.current_part, {"Material", "Color", "Alpha"})
+ end)
+ copymenu:AddOption("All appearance-related properties", function()
+ pace.MultiCopy(pace.current_part, {"Material", "Color", "Brightness", "Alpha", "Translucent", "BlendMode"})
+ end):SetTooltip("Material, Color, Brightness, Alpha, Translucent, Blend mode")
+ end
+end
+local function reformat_color(col, proper_in, proper_out)
+ local multiplier = 1
+ if not proper_in then multiplier = multiplier / 255 end
+ if not proper_out then multiplier = multiplier * 255 end
+ col.r = math.Clamp(col.r * multiplier,0,255)
+ col.g = math.Clamp(col.g * multiplier,0,255)
+ col.b = math.Clamp(col.b * multiplier,0,255)
+ col.a = math.Clamp(col.a * multiplier,0,255)
+ return col
+end
+local function do_multicopy()
+ if not pace.multicopy_source or not pace.multicopy_selected_properties then return end
+ for i,v in ipairs(pace.multicopy_selected_properties) do
+ local key = v[1]
+ local val = v[2] if not val then continue end if val == "" then continue end
+ if pace.current_part["Set"..key] then
+ if key == "Color" then
+ local color = pace.multicopy_source:GetColor()
+ local color_copy = Color(color.r,color.g,color.b)
+ reformat_color(color_copy, pace.multicopy_source.ProperColorRange, pace.current_part.ProperColorRange)
+ local vec = Vector(color_copy.r,color_copy.g,color_copy.b)
+ pace.current_part["Set"..key](pace.current_part,vec)
+ else
+ pace.current_part["Set"..key](pace.current_part,val)
+ end
+ end
+ end
+end
+
do -- base editable
local PANEL = {}
+
PANEL.ClassName = "properties_base_type"
PANEL.Base = "DLabel"
PANEL.SingleClick = true
+ function PANEL:Flash()
+ --redirect to the parent (container)
+ self:GetParent():Flash()
+ end
+
function PANEL:OnCursorMoved()
self:SetCursor("hand")
end
@@ -740,6 +1153,7 @@ do -- base editable
end
function PANEL:Init(...)
+ self.pac_property_panel = self
if DLabel and DLabel.Init then
local status = DLabel.Init(self, ...)
self:SetText('')
@@ -768,20 +1182,31 @@ do -- base editable
-- visually round numbers so 0.6 doesn't show up as 0.600000000001231231 on wear
value = math.Round(value, 7)
end
- local str = tostring(value)
+ local str = tostring(value) --this is the text that will end up on the display
+ local original_str = string.Trim(str,"\n") --this is the minimally-altered text that will remain as the internal value
+ local lines = string.Explode("\n", original_str)
+ if #lines > 1 then
+ str = ""
+ end
self:SetTextColor(self.alt_line and self:GetSkin().Colours.Category.AltLine.Text or self:GetSkin().Colours.Category.Line.Text)
+ if str == "" or self.used_by_proxy then
+ if pace.special_property_text_color then
+ self:SetTextColor(pace.special_property_text_color)
+ end
+ end
+
self:SetFont(pace.CurrentFont)
- self:SetText(" " .. str) -- ugh
+ self:SetText(" " .. string.Trim(str,"\n")) -- ugh
self:SizeToContents()
if #str > 10 then
- self:SetTooltip(str)
+ self:SetTooltip(original_str)
else
self:SetTooltip()
end
- self.original_str = str
+ self.original_str = original_str
self.original_var = var
if self.OnValueSet then
@@ -824,14 +1249,708 @@ do -- base editable
end
end
+ function pace.MultiCopy(part, tbl)
+ pace.clipboardtooltip = ""
+ pace.multicopy_selected_properties = {}
+ local str_tbl = {[1] = "multiple properties from " .. tostring(part)}
+ for i,v in ipairs(tbl) do
+ if part["Get"..v] then
+ local val = part["Get" .. v](part)
+ table.insert(pace.multicopy_selected_properties, {v, val})
+ table.insert(str_tbl,v .. " : " .. tostring(val))
+ end
+ end
+ pace.clipboardtooltip = table.concat(str_tbl, "\n")
+ pace.multicopying = true
+ pace.multicopy_source = part
+ end
function PANEL:PopulateContextMenu(menu)
- menu:AddOption(L"copy", function()
+ if self.user_proxies then
+ for _,part in pairs(self.user_proxies) do
+ menu:AddOption("jump to " .. tostring(part), function()
+ pace.GoToPart(part)
+ end):SetImage("icon16/arrow_turn_right.png")
+ end
+ end
+
+ if self.udata and self.udata.editor_panel == "part" then
+ if self:GetValue() ~= "" then
+ local part = pac.GetPartFromUniqueID(pac.Hash(pac.LocalPlayer), self:GetValue())
+ if IsValid(part) then
+ menu:AddOption("jump to " .. tostring(part), function()
+ pace.GoToPart(part)
+ end):SetImage("icon16/arrow_turn_right.png")
+ end
+ end
+ end
+
+ pace.clipboardtooltip = pace.clipboardtooltip or ""
+ local copymenu, copypnl = menu:AddSubMenu(L"copy", function()
pace.clipboard = pac.CopyValue(self:GetValue())
- end):SetImage(pace.MiscIcons.copy)
- menu:AddOption(L"paste", function()
- self:SetValue(pac.CopyValue(pace.clipboard))
- self.OnValueChanged(self:GetValue())
- end):SetImage(pace.MiscIcons.paste)
+ pace.clipboardtooltip = pace.clipboard .. " (from " .. tostring(pace.current_part) .. ")"
+ pace.multicopying = false
+ end) copypnl:SetImage(pace.MiscIcons.copy) copymenu.GetDeleteSelf = function() return false end
+ install_movable_multicopy(copymenu, self.CurrentKey)
+ install_appearance_multicopy(copymenu, self.CurrentKey)
+
+ local pnl = menu:AddOption(L"paste", function()
+ if pace.multicopying then
+ do_multicopy()
+ pace.PopulateProperties(pace.current_part)
+ else
+ self:SetValue(pac.CopyValue(pace.clipboard))
+ self.OnValueChanged(self:GetValue())
+ end
+ end) pnl:SetImage(pace.MiscIcons.paste) pnl:SetTooltip(pace.clipboardtooltip)
+
+ if #pace.BulkSelectList > 0 then
+ local uid_tbl = {}
+ local names_tbl = {}
+ for i,part in ipairs(pace.BulkSelectList) do
+ table.insert(uid_tbl, part.UniqueID)
+ table.insert(names_tbl, part:GetName())
+ end
+ local pnl = menu:AddOption(L"paste UID list", function()
+ self:SetValue(table.concat(uid_tbl,";"))
+ self.OnValueChanged(self:GetValue())
+ end) pnl:SetImage(pace.MiscIcons.paste) pnl:SetTooltip(table.concat(names_tbl,"\n"))
+ end
+
+ --command's String variable
+ if self.CurrentKey == "String" then
+
+ pace.bookmarked_ressources = pace.bookmarked_ressources or {}
+ pace.bookmarked_ressources["command"] =
+ {
+ --[[["user"] = {
+
+ },]]
+ ["basic lua"] = {
+ {
+ lua = true,
+ nicename = "if alive then say I\'m alive",
+ expression = "if LocalPlayer():Health() > 0 then print(\"I\'m alive\") RunConsoleCommand(\"say\", \"I\'m alive\") else RunConsoleCommand(\"say\", \"I\'m DEAD\") end",
+ explanation = "To showcase a basic if/else statement, this will make you say \"I'm alive\" or \"I\'m DEAD\" depending on whether you have more than 0 health."
+ },
+ {
+ lua = true,
+ nicename = "print 100 first numbers",
+ expression = "for i=0,100,1 do print(\"number\" .. i) end",
+ explanation = "To showcase a basic for loop (with the number setup), this will print the first 100 numbers in the console."
+ },
+ {
+ lua = true,
+ nicename = "print all entities' health",
+ expression = "for _,ent in pairs(ents.GetAll()) do print(ent, ent:Health()) end",
+ explanation = "To showcase a basic for loop (using a table iterator), this will print the list of all entities\' health"
+ },
+ {
+ lua = true,
+ nicename = "print all entities' health",
+ expression = "local random_n = 1 + math.floor(math.random()*5) RunConsoleCommand(\"pac_event\", \"event_\"..random_n)",
+ explanation = "To showcase basic number handling and variables, this will run a pac_event command for \"event_1\" to \"event_5\""
+ }
+ },
+ ["movement"] ={
+ {
+ lua = false,
+ nicename = "dash",
+ expression = "+forward;+speed",
+ explanation = "go forward. WARNING. It holds forever until you release it with -forward;-speed"
+ },
+ },
+ ["weapons"] = {
+ {
+ lua = false,
+ nicename = "go unarmed (using console)",
+ expression = "give none; use none",
+ explanation = "use the hands swep (\"none\"). In truth, we need to split the command and run the second one after a delay, or run the full thing twice. the console doesn't let us switch to a weapon we don't yet have"
+ },
+ {
+ lua = true,
+ nicename = "go unarmed (using lua)",
+ expression = "RunConsoleCommand(\"give\", \"none\") timer.Simple(0.1, function() RunConsoleCommand(\"use\", \"none\") end)",
+ explanation = "use the hands swep (\"none\"). we need lua because the console doesn't let us switch to a weapon we don't yet have"
+ }
+ },
+ ["events logic"] = {
+ {
+ lua = true,
+ nicename = "random command event activation",
+ expression = "RunConsoleCommand(\"pac_event\", \"COMMAND\" .. math.ceil(math.random()*4))",
+ explanation = "randomly pick between commands COMMAND1 to COMMAND4.\nReplace 4 to another whole number if you need more or less"
+ },
+ {
+ lua = true,
+ nicename = "command series (held down)",
+ expression = "local i = LocalPlayer()[\"COMMAND\"] RunConsoleCommand(\"pac_event\", \"COMMAND\" .. i, \"1\") RunConsoleCommand(\"pac_event\", \"COMMAND\" .. i-1, \"0\") if i > 5 then i = 0 end LocalPlayer()[\"COMMAND\"] = i + 1",
+ explanation = "goes in the series of COMMAND1 to COMMAND5 activating the current number and deactivating the previous.\nYou can replace COMMAND for another name, and replace the i > 5 for another limit to loop back around\nAlthough now you can use pac_event_sequenced to control event series"
+ },
+ {
+ lua = true,
+ nicename = "command series (impulse)",
+ expression = "local i = LocalPlayer()[\"COMMAND\"] RunConsoleCommand(\"pac_event\", \"COMMAND\" .. i) if i >= 5 then i = 0 end LocalPlayer()[\"COMMAND\"] = i + 1",
+ explanation = "goes in the series of COMMAND1 to COMMAND5 activating one command instantaneously.\nYou can replace COMMAND for another name, and replace the i >= 5 for another limit to loop back around"
+ },
+ {
+ lua = nil,
+ nicename = "save current events to a single command",
+ explanation = "this hardcoded preset should build a list of all your active command events and save it as a single command string for you"
+ }
+ },
+ --[[["experimental things"] = {
+ {
+ nicename = "",
+ expression = "",
+ explanation = ""
+ },
+ }]]
+ }
+
+ local menu1, pnl1 = menu:AddSubMenu(L"example commands", function()
+ end)
+ pnl1:SetIcon("icon16/cart_go.png")
+ for group, tbl in pairs(pace.bookmarked_ressources["command"]) do
+ local icon = "icon16/bullet_white.png"
+ if group == "user" then icon = "icon16/user.png"
+ elseif group == "movement" then icon = "icon16/user_go.png"
+ elseif group == "weapons" then icon = "icon16/bomb.png"
+ elseif group == "events logic" then icon = "icon16/clock.png"
+ elseif group == "spatial" then icon = "icon16/world.png"
+ elseif group == "experimental things" then icon = "icon16/ruby.png"
+ end
+ local menu2, pnl2 = menu1:AddSubMenu(group)
+ pnl2:SetIcon(icon)
+
+ if not table.IsEmpty(tbl) then
+ for i,tbl2 in pairs(tbl) do
+ --print(tbl2.nicename)
+ local str = tbl2.nicename or "invalid name"
+ local pnl3 = menu2:AddOption(str, function()
+ if pace.current_part.ClassName == "command" then
+ local expression = pace.current_part.String
+ local hardcode = tbl2.lua == nil
+ local new_expression = ""
+ if hardcode then
+
+ if tbl2.nicename == "save current events to a single command" then
+ local tbl3 = {}
+ for i,v in pairs(LocalPlayer().pac_command_events) do tbl3[i] = v.on end
+ for i,v in pairs(LocalPlayer().pac_command_events) do RunConsoleCommand("pac_event", i, "0") end
+ new_expression = ""
+
+ for i,v in pairs(tbl3) do new_expression = new_expression .. "pac_event " .. i .. " " .. v .. ";" end
+ pace.current_part:SetUseLua(false)
+ end
+
+ end
+ if expression == "" then --blank: bare insert
+ expression = tbl2.expression
+ pace.current_part:SetUseLua(tbl2.lua)
+ elseif pace.current_part.UseLua == tbl2.lua then --something present: concatenate the existing bit but only if we're on the same mode
+ expression = expression .. ";" .. tbl2.expression
+ pace.current_part:SetUseLua(tbl2.lua)
+ end
+
+ if not hardcode then
+ pace.current_part:SetString(expression)
+ self:SetValue(expression)
+ else
+ pace.current_part:SetString(new_expression)
+ self:SetValue(new_expression)
+ end
+ end
+
+ end)
+ pnl3:SetIcon(icon)
+ pnl3:SetTooltip(tbl2.explanation)
+ end
+
+ end
+ end
+ end
+
+ --proxy expression
+ if self.CurrentKey == "Expression" then
+
+
+ pace.bookmarked_ressources = pace.bookmarked_ressources or {}
+ pace.bookmarked_ressources["proxy"] = pace.bookmarked_ressources["proxy"]
+ local menu1, pnl1 = menu:AddSubMenu(L"Proxy template bits", function()
+ end)
+ pnl1:SetIcon("icon16/cart_go.png")
+ for group, tbl in pairs(pace.bookmarked_ressources["proxy"]) do
+ local icon = "icon16/bullet_white.png"
+ if group == "user" then icon = "icon16/user.png"
+ elseif group == "fades and transitions" then icon = "icon16/shading.png"
+ elseif group == "pulses" then icon = "icon16/transmit_blue.png"
+ elseif group == "facial expressions" then icon = "icon16/emoticon_smile.png"
+ elseif group == "spatial" then icon = "icon16/world.png"
+ elseif group == "experimental things" then icon = "icon16/ruby.png"
+ end
+ local menu2, pnl2 = menu1:AddSubMenu(group)
+ pnl2:SetIcon(icon)
+
+ if not table.IsEmpty(tbl) then
+ for i,tbl2 in pairs(tbl) do
+ --print(tbl2.nicename)
+ local str = tbl2.nicename or "invalid name"
+ local pnl3 = menu2:AddOption(str, function()
+ if pace.current_part.ClassName == "proxy" then
+ local expression = pace.current_part.Expression
+ if expression == "" then --blank: bare insert
+ expression = tbl2.expression
+ elseif true then --something present: multiply the existing bit?
+ expression = expression .. " * " .. tbl2.expression
+ end
+
+ pace.current_part:SetExpression(expression)
+ self:SetValue(expression)
+ end
+
+ end)
+ pnl3:SetIcon(icon)
+ pnl3:SetTooltip(tbl2.explanation)
+ end
+
+ end
+ end
+
+ local tutorials, pnl2 = menu:AddSubMenu(L"Tutorials for the active functions")
+ for i, kw in ipairs(pace.current_part:GetActiveFunctions()) do
+ pace.current_part.errors_override = true --hack to stop competing SetInfo, SetWarning and SetError buttons
+ local tutorial = pace.current_part:GetTutorial(kw) if tutorial == nil then continue end
+ local pnl3 = tutorials:AddOption(kw, function()
+ pace.alternate_message_prompts = true
+ pace.current_part:SetInfo(tutorial)
+ pace.current_part:AttachEditorPopup(tutorial, true)
+ end) pnl3:SetIcon("icon16/calculator.png")
+ pnl3:SetTooltip(tutorial)
+ end
+ pnl2:SetImage("icon16/information.png")
+ end
+
+ if self.CurrentKey == "Function" or self.CurrentKey == "Input" then
+ local proxy = pace.current_part
+ menu:AddOption("Translate easy setup into an expression", function()
+ proxy:SetExpression(
+ proxy.Min .. " + (" .. proxy.Max .. "-" .. proxy.Min .. ") * (" ..
+ "(" .. proxy.Function .. "(((" .. proxy.Input .. "()/" .. proxy.InputDivider .. ") + " .. proxy.Offset .. ") * " ..
+ proxy.InputMultiplier .. ") + 1) / 2) ^" .. proxy.Pow
+ )
+ pace.PopulateProperties(proxy)
+ end):SetIcon("icon16/calculator.png")
+
+ local tutorials, pnl2 = menu:AddSubMenu(L"Tutorials for the active functions")
+ for i, kw in ipairs(pace.current_part:GetActiveFunctions()) do
+ pace.current_part.errors_override = true --hack to stop competing SetInfo, SetWarning and SetError buttons
+ local tutorial = pace.current_part:GetTutorial(kw) if tutorial == nil then continue end
+ local pnl3 = tutorials:AddOption(kw, function()
+ pace.alternate_message_prompts = true
+ pace.current_part:SetInfo(tutorial)
+ pace.current_part:AttachEditorPopup(tutorial, true)
+ end) pnl3:SetIcon("icon16/calculator.png")
+ pnl3:SetTooltip(tutorial)
+ end
+ pnl2:SetTooltip(pace.current_part:GetTutorial(pace.current_part[self.CurrentKey]))
+ pnl2:SetImage("icon16/information.png")
+ end
+
+ if self.CurrentKey == "LoadVmt" then
+ local inserted_mat_owners = {}
+
+ local owner = pace.current_part:GetOwner()
+ local name = string.GetFileFromFilename( owner:GetModel() )
+ local mats = owner:GetMaterials()
+
+ local pnl, menu2 = menu:AddSubMenu(L"Load " .. name .. "'s material", function()
+ end)
+ menu2:SetImage("icon16/paintcan.png")
+ inserted_mat_owners[owner:GetModel()] = true
+
+ for id,mat in ipairs(mats) do
+ pnl:AddOption(string.GetFileFromFilename(mat), function()
+ pace.current_part:SetLoadVmt(mat)
+ end)
+ end
+
+ --add parent owners (including the owner entity at root)
+ for i,part in ipairs(pace.current_part:GetParentList()) do
+ local owner = part:GetOwner()
+ local name = string.GetFileFromFilename( owner:GetModel() )
+ local mats = owner:GetMaterials()
+ if not inserted_mat_owners[owner:GetModel()] then
+ local pnl, menu2 = menu:AddSubMenu(L"Load " .. name .. "'s material", function()
+ end)
+ menu2:SetImage("icon16/paintcan.png")
+ inserted_mat_owners[owner:GetModel()] = true
+
+ for id,mat in ipairs(mats) do
+ pnl:AddOption(string.GetFileFromFilename(mat), function()
+ pace.current_part:SetLoadVmt(mat)
+ end)
+ end
+ end
+
+ end
+ end
+
+ if self.CurrentKey == "SurfaceProperties" and pace.current_part.GetSurfacePropsTable then
+ local tbl = pace.current_part:GetSurfacePropsTable()
+ menu:AddOption(L"See physics info", function()
+ local pnl2 = vgui.Create("DFrame")
+ local txt_zone = vgui.Create("DTextEntry", pnl2)
+ local str = ""
+ for i,v in pairs(tbl) do
+ str = str .. i .. " = " .. v .."\n"
+ end
+ txt_zone:SetMultiline(true)
+ txt_zone:SetText(str)
+ txt_zone:Dock(FILL)
+ pnl2:SetTitle("SurfaceProp info : " .. pace.current_part.SurfaceProperties)
+ pnl2:SetSize(500, 500)
+ pnl2:SetPos(ScrW()/2, ScrH()/2)
+ pnl2:MakePopup()
+
+ end):SetImage("icon16/table.png")
+
+ end
+
+ if self.CurrentKey == "Model" then
+ pace.bookmarked_ressources = pace.bookmarked_ressources or {}
+ if not pace.bookmarked_ressources["models"] then
+ pace.bookmarked_ressources["models"] = {
+ "models/pac/default.mdl",
+ "models/pac/plane.mdl",
+ "models/pac/circle.mdl",
+ "models/hunter/blocks/cube025x025x025.mdl",
+ "models/editor/axis_helper.mdl",
+ "models/editor/axis_helper_thick.mdl"
+ }
+ end
+
+ local menu2, pnl = menu:AddSubMenu(L"Load favourite models", function()
+ end)
+ pnl:SetImage("icon16/cart_go.png")
+
+ local pm = pace.current_part:GetPlayerOwner():GetModel()
+ local pm_selected = player_manager.TranslatePlayerModel(GetConVar("cl_playermodel"):GetString())
+
+ if pm_selected ~= pm then
+ menu2:AddOption("Selected playermodel - " .. string.gsub(string.GetFileFromFilename(pm_selected), ".mdl", ""), function()
+ pace.current_part:SetModel(pm_selected)
+ pace.current_part.pace_properties["Model"]:SetValue(pm_selected)
+ pace.PopulateProperties(pace.current_part)
+
+ end):SetImage("materials/spawnicons/"..string.gsub(pm_selected, ".mdl", "")..".png")
+ end
+
+ if IsValid(pace.current_part:GetRootPart():GetOwner()) then
+ local root_model = pace.current_part:GetRootPart():GetOwner():GetModel()
+ if root_model ~= pm then
+ if not file.Exists("materials/spawnicons/"..string.gsub(root_model, ".mdl", "")..".png", "GAME") then
+ pace.FlashNotification("missing spawn icon")
+ local spawnicon = vgui.Create("SpawnIcon")
+ spawnicon:SetPos(0,0)
+ spawnicon:SetModel(root_model)
+ spawnicon:RebuildSpawnIcon()
+ timer.Simple(2, function()
+ spawnicon:Remove()
+ end)
+ end
+ local pnl = menu2:AddOption("root owner model - " .. string.gsub(string.GetFileFromFilename(root_model), ".mdl", ""), function()
+ pace.current_part:SetModel(root_model)
+ pace.current_part.pace_properties["Model"]:SetValue(root_model)
+ pace.PopulateProperties(pace.current_part)
+
+ end)
+ pnl:SetImage("materials/spawnicons/"..string.gsub(root_model, ".mdl", "")..".png")
+ timer.Simple(0, function()
+ pnl:SetImage("materials/spawnicons/"..string.gsub(root_model, ".mdl", "")..".png")
+ end)
+ end
+ end
+
+ menu2:AddOption("Active playermodel - " .. string.gsub(string.GetFileFromFilename(pm), ".mdl", ""), function()
+ pace.current_part:SetModel(pm)
+ pace.current_part.pace_properties["Model"]:SetValue(pm)
+ pace.PopulateProperties(pace.current_part)
+ end):SetImage("materials/spawnicons/"..string.gsub(pm, ".mdl", "")..".png")
+
+ if IsValid(pac.LocalPlayer:GetActiveWeapon()) then
+ local wep = pac.LocalPlayer:GetActiveWeapon()
+ local wep_mdl = wep:GetModel()
+ menu2:AddOption("Active weapon - " .. wep:GetClass() .. " - model - " .. string.gsub(string.GetFileFromFilename(wep_mdl), ".mdl", ""), function()
+ pace.current_part:SetModel(wep_mdl)
+ pace.current_part.pace_properties["Model"]:SetValue(wep_mdl)
+ pace.PopulateProperties(pace.current_part)
+ end):SetImage("materials/spawnicons/"..string.gsub(wep_mdl, ".mdl", "")..".png")
+ end
+
+ for id,mdl in ipairs(pace.bookmarked_ressources["models"]) do
+ if string.sub(mdl, 1, 7) == "folder:" then
+ mdl = string.sub(mdl, 8, #mdl)
+ local menu3, pnl2 = menu2:AddSubMenu(string.GetFileFromFilename(mdl), function()
+ end)
+ pnl2:SetImage("icon16/folder.png")
+
+ local files = get_files_recursively(nil, mdl, "mdl")
+
+ for i,file in ipairs(files) do
+ menu3:AddOption(string.GetFileFromFilename(file), function()
+ self:SetValue(file)
+ pace.current_part:SetModel(file)
+ timer.Simple(0.2, function()
+ pace.current_part.pace_properties["Model"]:SetValue(file)
+ pace.PopulateProperties(pace.current_part)
+ end)
+ end):SetImage("materials/spawnicons/"..string.gsub(file, ".mdl", "")..".png")
+ end
+ else
+ menu2:AddOption(string.GetFileFromFilename(mdl), function()
+ self:SetValue(mdl)
+ pace.current_part:SetModel(mdl)
+ timer.Simple(0.2, function()
+ pace.current_part.pace_properties["Model"]:SetValue(mdl)
+ pace.PopulateProperties(pace.current_part)
+ end)
+ end):SetImage("materials/spawnicons/"..string.gsub(mdl, ".mdl", "")..".png")
+ end
+ end
+ end
+
+ if self.CurrentKey == "Material" or self.CurrentKey == "SpritePath" then
+ pace.bookmarked_ressources = pace.bookmarked_ressources or {}
+ if not pace.bookmarked_ressources["materials"] then
+ pace.bookmarked_ressources["materials"] = {
+ "models/debug/debugwhite.vmt",
+ "vgui/null.vmt",
+ "debug/env_cubemap_model.vmt",
+ "models/wireframe.vmt",
+ "cable/physbeam.vmt",
+ "cable/cable2.vmt",
+ "effects/tool_tracer.vmt",
+ "effects/flashlight/logo.vmt",
+ "particles/flamelet[1,5]",
+ "sprites/key_[0,9]",
+ "vgui/spawnmenu/generating.vmt",
+ "vgui/spawnmenu/hover.vmt",
+ "metal"
+ }
+ end
+
+ local menu2, pnl = menu:AddSubMenu(L"Load favourite materials", function()
+ end)
+ pnl:SetImage("icon16/cart_go.png")
+
+ for id,mat in ipairs(pace.bookmarked_ressources["materials"]) do
+ mat = string.gsub(mat, "^materials/", "")
+ local mat_no_ext = string.StripExtension(mat)
+
+ if string.find(mat, "%[%d+,%d+%]") then --find the bracket notation
+ mat_no_ext = string.gsub(mat_no_ext, "%[%d+,%d+%]", "")
+ pace.AddSubmenuWithBracketExpansion(menu2, function(str)
+ str = str or ""
+ str = string.StripExtension(string.gsub(str, "^materials/", ""))
+ self:SetValue(str)
+ if self.CurrentKey == "Material" then
+ pace.current_part:SetMaterial(str)
+ elseif self.CurrentKey == "SpritePath" then
+ pace.current_part:SetSpritePath(str)
+ end
+ end, mat_no_ext, "vmt", "materials")
+
+ else
+ menu2:AddOption(string.StripExtension(mat), function()
+ self:SetValue(mat_no_ext)
+ if self.CurrentKey == "Material" then
+ pace.current_part:SetMaterial(mat_no_ext)
+ elseif self.CurrentKey == "SpritePath" then
+ pace.current_part:SetSpritePath(mat_no_ext)
+ end
+ end):SetMaterial(mat)
+ end
+
+ end
+
+ local pac_materials = {}
+ local has_pac_materials = false
+
+ local class_shaders = {
+ ["material"] = "VertexLitGeneric",
+ ["material_3d"] = "VertexLitGeneric",
+ ["material_2d"] = "UnlitGeneric",
+ ["material_eye refract"] = "EyeRefract",
+ ["material_refract"] = "Refract",
+ }
+
+ for _,part in pairs(pac.GetLocalParts()) do
+ if part.Name ~= "" and string.find(part.ClassName, "material") then
+ if pac_materials[class_shaders[part.ClassName]] == nil then pac_materials[class_shaders[part.ClassName]] = {} end
+ has_pac_materials = true
+ pac_materials[class_shaders[part.ClassName]][part:GetName()] = {part = part, shader = class_shaders[part.ClassName]}
+ end
+ end
+ if has_pac_materials then
+ menu2:AddSpacer()
+ for shader,mats in pairs(pac_materials) do
+ local shader_submenu = menu2:AddSubMenu("pac3 materials - " .. shader)
+ for mat,tbl in pairs(mats) do
+ local part = tbl.part
+ local pnl2 = shader_submenu:AddOption(mat, function()
+ self:SetValue(mat)
+ if self.CurrentKey == "Material" then
+ pace.current_part:SetMaterial(mat)
+ elseif self.CurrentKey == "SpritePath" then
+ pace.current_part:SetSpritePath(mat)
+ end
+ end)
+ pnl2:SetMaterial(pac.Material(mat, part))
+ pnl2:SetTooltip(tbl.shader)
+ end
+ end
+ end
+
+ if self.CurrentKey == "Material" and pace.current_part.ClassName == "particles" then
+ pnl:SetTooltip("Appropriate shaders for particles are UnlitGeneric materials.\nOOtherwise, they should usually be additive or use VertexAlpha")
+ elseif self.CurrentKey == "SpritePath" then
+ pnl:SetTooltip("Appropriate shaders for sprites are UnlitGeneric materials.\nOOtherwise, they should usually be additive or use VertexAlpha")
+ end
+ end
+
+ if string.find(pace.current_part.ClassName, "sound") then
+ if self.CurrentKey == "Sound" or self.CurrentKey == "Path" then
+ pace.bookmarked_ressources = pace.bookmarked_ressources or {}
+ if not pace.bookmarked_ressources["sound"] then
+ pace.bookmarked_ressources["sound"] = {
+ "music/hl1_song11.mp3",
+ "music/hl2_song23_suitsong3.mp3",
+ "music/hl2_song1.mp3",
+ "npc/combine_gunship/dropship_engine_near_loop1.wav",
+ "ambient/alarms/warningbell1.wav",
+ "phx/epicmetal_hard7.wav",
+ "phx/explode02.wav"
+ }
+ end
+
+ local menu2, pnl = menu:AddSubMenu(L"Load favourite sounds", function()
+ end)
+ pnl:SetImage("icon16/cart_go.png")
+
+ for id,snd in ipairs(pace.bookmarked_ressources["sound"]) do
+ local extension = string.GetExtensionFromFilename(snd)
+ local snd_no_ext = string.StripExtension(snd)
+ local single_menu = not favorites_menu_expansion:GetBool()
+
+ if string.sub(snd, 1, 7) == "folder:" then
+ snd = string.sub(snd, 8, #snd)
+ local menu3, pnl2 = menu2:AddSubMenu(string.GetFileFromFilename(snd), function()
+ end)
+ pnl2:SetImage("icon16/folder.png") pnl2:SetTooltip(snd)
+
+ local files = get_files_recursively(nil, snd, {"wav", "mp3", "ogg"})
+
+ for i,file in ipairs(files) do
+ file = string.sub(file,7,#file) --"sound/"
+ local icon = "icon16/sound.png"
+ if string.find(file, "music") or string.find(file, "theme") then
+ icon = "icon16/music.png"
+ elseif string.find(file, "loop") then
+ icon = "icon16/arrow_rotate_clockwise.png"
+ end
+ local pnl3 = menu3:AddOption(string.GetFileFromFilename(file), function()
+ self:SetValue(file)
+ if self.CurrentKey == "Sound" then
+ pace.current_part:SetSound(file)
+ elseif self.CurrentKey == "Path" then
+ pace.current_part:SetPath(file)
+ end
+ end)
+ pnl3:SetImage(icon) pnl3:SetTooltip(file)
+ end
+ elseif string.find(snd_no_ext, "%[%d+,%d+%]") then --find the bracket notation
+ pace.AddSubmenuWithBracketExpansion(menu2, function(str)
+ self:SetValue(str)
+ if self.CurrentKey == "Sound" then
+ pace.current_part:SetSound(str)
+ elseif self.CurrentKey == "Path" then
+ pace.current_part:SetPath(str)
+ end
+ end, snd_no_ext, extension, "sound")
+
+ elseif not single_menu and string.find(snd_no_ext, "%d+") then --find a file ending in a number
+ --expand only if we want it with the cvar
+ pace.AddSubmenuWithBracketExpansion(menu2, function(str)
+ self:SetValue(str)
+ if self.CurrentKey == "Sound" then
+ pace.current_part:SetSound(str)
+ elseif self.CurrentKey == "Path" then
+ pace.current_part:SetPath(str)
+ end
+ end, snd_no_ext, extension, "sound")
+
+ else
+
+ local icon = "icon16/sound.png"
+
+ if string.find(snd, "music") or string.find(snd, "theme") then
+ icon = "icon16/music.png"
+ elseif string.find(snd, "loop") then
+ icon = "icon16/arrow_rotate_clockwise.png"
+ end
+
+ menu2:AddOption(snd, function()
+ self:SetValue(snd)
+ if self.CurrentKey == "Sound" then
+ pace.current_part:SetSound(snd)
+ elseif self.CurrentKey == "Path" then
+ pace.current_part:SetPath(snd)
+ end
+
+ end):SetIcon(icon)
+ end
+
+
+ end
+ end
+ end
+
+ --long string menu to bypass the DLabel's limits for some fields
+ if (pace.current_part.ClassName == "sound2" and self.CurrentKey == "Path") or self.CurrentKey == "Notes" or (pace.current_part.ClassName == "text" and self.CurrentKey == "Text")
+ or (pace.current_part.ClassName == "command" and self.CurrentKey == "String")
+ or self.CurrentKey == "Expression" or self.CurrentKey == "ExpressionOnHide" or self.CurrentKey == "Extra1" or self.CurrentKey == "Extra2" or self.CurrentKey == "Extra3" or self.CurrentKey == "Extra4" or self.CurrentKey == "Extra5" then
+ menu:AddOption(L"Insert long text", function()
+ local pnl = vgui.Create("DFrame")
+ local DText = vgui.Create("DTextEntry", pnl)
+ local DButtonOK = vgui.Create("DButton", pnl)
+ DText:SetMaximumCharCount(50000)
+
+ pnl:SetSize(1200,800)
+ pnl:SetTitle("Long text with newline support for " .. self.CurrentKey .. ". Do not touch the label after this!")
+ pnl:SetPos(200, 100)
+ DButtonOK:SetText("OK")
+ DButtonOK:SetSize(80,20)
+ DButtonOK:SetPos(500, 775)
+ DText:SetPos(5,25)
+ DText:SetSize(1190,700)
+ DText:SetMultiline(true)
+ DText:SetContentAlignment(7)
+ pnl:MakePopup()
+ DText:RequestFocus()
+ DText:SetText(pace.current_part[self.CurrentKey])
+
+ DButtonOK.DoClick = function()
+ local str = DText:GetText()
+ pace.current_part[self.CurrentKey] = str
+ if pace.current_part.ClassName == "sound2" then
+ pace.current_part.AllPaths = str
+ pace.current_part:UpdateSoundsFromAll()
+ end
+ pace.PopulateProperties(pace.current_part)
+ pnl:Remove()
+ end
+ end):SetImage('icon16/text_letter_omega.png')
+ end
--left right swap available on strings (and parts)
if type(self:GetValue()) == 'string' then
@@ -871,6 +1990,27 @@ do -- base editable
self:SetValue(-val)
self.OnValueChanged(self:GetValue())
end):SetImage("icon16/arrow_switch.png")
+
+ if self.CurrentKey == "Size" then
+ if pace.current_part.ClassName == "sprite" then
+ menu:AddOption(L"apply size to scales", function()
+ local val = self:GetValue()
+ pace.current_part.SizeX = pace.current_part.SizeX * val
+ pace.current_part.SizeY = pace.current_part.SizeX * val
+ self:SetValue(1)
+ self.OnValueChanged(self:GetValue())
+ pace.PopulateProperties(pace.current_part)
+ end):SetImage("icon16/arrow_down.png")
+ elseif pace.current_part.SetScale and pace.current_part.GetScale then
+ menu:AddOption(L"apply size to scales", function()
+ local val = self:GetValue()
+ pace.current_part:SetScale(val * pace.current_part:GetScale())
+ self:SetValue(1)
+ self.OnValueChanged(self:GetValue())
+ pace.PopulateProperties(pace.current_part)
+ end):SetImage("icon16/arrow_down.png")
+ end
+ end
end
menu:AddSpacer()
@@ -936,6 +2076,7 @@ do -- base editable
local hookID = tostring({})
local textEntry = pnl
local delay = os.clock() + 0.1
+ local inset_x = self:GetTextInset()
pac.AddHook('Think', hookID, function(code)
if not IsValid(self) or not IsValid(textEntry) then return pac.RemoveHook('Think', hookID) end
@@ -954,7 +2095,6 @@ do -- base editable
--pnl:SetPos(x+3,y-4)
--pnl:Dock(FILL)
local x, y = self:LocalToScreen()
- local inset_x = self:GetTextInset()
pnl:SetPos(x+5 + inset_x, y)
pnl:SetSize(self:GetSize())
pnl:SetWide(ScrW())
@@ -973,6 +2113,11 @@ do -- base editable
local old = pnl.Paint
pnl.Paint = function(...)
if not self:IsValid() then pnl:Remove() return end
+ local x, y = self:LocalToScreen()
+ local _,prop_y = pace.properties:LocalToScreen(0,0)
+ y = math.Clamp(y,prop_y,ScrH() - self:GetTall())
+
+ pnl:SetPos(x + 5 + inset_x, y)
surface.SetFont(pnl:GetFont())
local w = surface.GetTextSize(pnl:GetText()) + 6
@@ -986,6 +2131,34 @@ do -- base editable
old(...)
end
+ local skincolor = self:GetSkin().Colours.Category.Line.Button
+ local col = Color(skincolor.r,skincolor.g,skincolor.b, 255)
+
+ --draw a rectangle with property key's name and arrows to show where the line is scrolling out of bounds
+ pac.AddHook('PostRenderVGUI', hookID .. "2", function(code)
+ if not IsValid(self) or not IsValid(pnl) then pac.RemoveHook('Think', hookID .. "2") return end
+ local _,prop_y = pace.properties:LocalToScreen(0,0)
+ local x, y = self:LocalToScreen()
+ local overflow = y < prop_y or y > ScrH() - self:GetTall()
+ if overflow then
+ local str = ""
+ if y > ScrH() then
+ str = "↓↓ " .. " " .. self.CurrentKey .. " " .. " ↓↓"
+ else
+ str = "↑↑ " .. " " .. self.CurrentKey .. " " .. " ↑↑"
+ end
+ y = math.Clamp(y,prop_y,ScrH() - self:GetTall())
+ surface.SetFont(pnl:GetFont())
+ local w2 = surface.GetTextSize(str)
+
+ surface.SetDrawColor(col)
+ surface.DrawRect(x - w2, y, w2, pnl:GetTall())
+ surface.SetTextColor(self:GetSkin().Colours.Category.Line.Text)
+ surface.SetTextPos(x - w2, y)
+ surface.DrawText(str)
+ end
+ end)
+
pace.BusyWithProperties = pnl
end
@@ -1061,6 +2234,13 @@ do -- vector
local left = pace.CreatePanel("properties_number", self)
local middle = pace.CreatePanel("properties_number", self)
local right = pace.CreatePanel("properties_number", self)
+ --a hack so that the scrolling out-of-bounds indicator rectangle with arrows has the key
+ timer.Simple(0, function()
+ if not IsValid(left) then return end
+ left.CurrentKey = self.CurrentKey
+ middle.CurrentKey = self.CurrentKey
+ right.CurrentKey = self.CurrentKey
+ end)
left.PopulateContextMenu = function(_, menu) self:PopulateContextMenu(menu) end
middle.PopulateContextMenu = function(_, menu) self:PopulateContextMenu(menu) end
@@ -1122,6 +2302,11 @@ do -- vector
self.middle = middle
self.right = right
+ self.pac_property_panel = self
+ left.pac_property_panel = self
+ middle.pac_property_panel = self
+ right.pac_property_panel = self
+
if self.MoreOptionsLeftClick then
local btn = vgui.Create("DButton", self)
btn:SetSize(16, 16)
@@ -1142,6 +2327,95 @@ do -- vector
surface.SetDrawColor(self:GetSkin().Colours.Properties.Border)
surface.DrawOutlinedRect(0,0,w,h)
end
+
+ --screen color picker
+ local btn2 = vgui.Create("DImageButton", self)
+ btn2:SetSize(16, 16)
+ btn2:Dock(RIGHT) btn2:DockPadding(0,0,16,0)
+ btn2:SetTooltip("Color picker")
+ btn2:SetImage("icon16/sitemap_color.png")
+ btn2.DoClick = function()
+ pace.FlashNotification("Hold Left Shift to open a circle to average nearby pixels")
+ local averaging_radius = 0
+ local lock_x
+ local lock_y
+ pac.AddHook("DrawOverlay", "colorpicker", function()
+ render.CapturePixels()
+ local mx, my = input.GetCursorPos()
+ local cx = mx
+ local cy = my
+
+ local r,g,b,a = render.ReadPixel(mx,my)
+
+ --we may average on nearby pixels
+ if input.IsKeyDown(KEY_LSHIFT) then
+ lock_x = lock_x or mx
+ lock_y = lock_y or my
+ local dx = mx-lock_x
+ local dy = my-lock_y
+
+ averaging_radius = math.floor(math.sqrt(dx^2 + dy^2))
+ else
+ lock_x = nil
+ lock_y = nil
+ averaging_radius = 0
+ end
+ if lock_x and lock_y then
+ cx = lock_x
+ cy = lock_y
+ end
+ local r_sum = 0
+ local g_sum = 0
+ local b_sum = 0
+
+ if averaging_radius > 0 then
+ local counted_pixels = 0
+ for x=cx-averaging_radius,mx+averaging_radius,1 do
+ for y=cy-averaging_radius,my+averaging_radius,1 do
+ if x^2 + y^2 > averaging_radius^2 then
+ counted_pixels = counted_pixels + 1
+ local r,g,b,a = render.ReadPixel(x,y)
+ r_sum = r_sum + r
+ g_sum = g_sum + g
+ b_sum = b_sum + b
+ end
+ end
+ end
+ if counted_pixels ~= 0 then
+ r = math.floor(r_sum / counted_pixels)
+ g = math.floor(g_sum / counted_pixels)
+ b = math.floor(b_sum / counted_pixels)
+ end
+
+ if RealTime() % 0.2 > 0.1 then
+ surface.DrawCircle(cx, cy, averaging_radius, 0,0,0,255)
+ else
+ surface.DrawCircle(cx, cy, averaging_radius, 255,255,255,255)
+ end
+ draw.DrawText("(average color) radius = "..averaging_radius, "TargetID", cx + 15, cy + 15, picked_color, TEXT_ALIGN_LEFT)
+ end
+
+ local color = Color(0,0,0)
+ if r + g + a < 400 then
+ color = Color(255,255,255)
+ end
+
+ local picked_color = Color(r,g,b)
+ draw.RoundedBox(0,cx + 15,cy,120,18,color)
+ draw.DrawText(r .. " " .. g .. " " .. " " .. b, "TargetID", cx + 15, cy, picked_color, TEXT_ALIGN_LEFT)
+ if input.IsMouseDown(MOUSE_LEFT) then
+ pac.CopyValue(picked_color)
+ if pace.current_part.ProperColorRange then
+ self.OnValueChanged(Vector(picked_color.r/255,picked_color.g/255,picked_color.b/255))
+ else
+ self.OnValueChanged(Vector(picked_color.r,picked_color.g,picked_color.b))
+ end
+
+ pac.RemoveHook("DrawOverlay", "colorpicker")
+ end
+ if input.IsKeyDown(KEY_ESCAPE) then pac.RemoveHook("DrawOverlay", "colorpicker") end
+ end)
+ end
end
end
@@ -1165,25 +2439,43 @@ do -- vector
end
function PANEL:PopulateContextMenu(menu)
- menu:AddOption(L"copy", function()
- pace.clipboard = pac.CopyValue(self.vector)
- end):SetImage(pace.MiscIcons.copy)
- menu:AddOption(L"paste", function()
- local val = pac.CopyValue(pace.clipboard)
- if isnumber(val) then
- val = ctor(val, val, val)
- elseif isvector(val) and type == "angle" then
- val = ctor(val.x, val.y, val.z)
- elseif isangle(val) and type == "vector" then
- val = ctor(val.p, val.y, val.r)
+ if self.user_proxies then
+ for _,part in pairs(self.user_proxies) do
+ menu:AddOption("jump to " .. tostring(part), function()
+ pace.GoToPart(part)
+ end):SetImage("icon16/arrow_turn_right.png")
end
+ end
+ pace.clipboardtooltip = pace.clipboardtooltip or ""
+ local copymenu, copypnl = menu:AddSubMenu(L"copy", function()
+ pace.clipboard = pac.CopyValue(self.vector)
+ pace.clipboardtooltip = tostring(pace.clipboard) .. " (from " .. tostring(pace.current_part) .. ")"
+ pace.multicopying = false
+ end) copypnl:SetImage(pace.MiscIcons.copy) copymenu.GetDeleteSelf = function() return false end
+ install_movable_multicopy(copymenu, self.CurrentKey)
+ install_appearance_multicopy(copymenu, self.CurrentKey)
+
+ local pnl = menu:AddOption(L"paste", function()
+ if pace.multicopying then
+ do_multicopy()
+ pace.PopulateProperties(pace.current_part)
+ else
+ local val = pac.CopyValue(pace.clipboard)
+ if isnumber(val) then
+ val = ctor(val, val, val)
+ elseif isvector(val) and type == "angle" then
+ val = ctor(val.x, val.y, val.z)
+ elseif isangle(val) and type == "vector" then
+ val = ctor(val.p, val.y, val.r)
+ end
- if _G.type(val):lower() == type or type == "color" then
- self:SetValue(val)
+ if _G.type(val):lower() == type or type == "color" or type == "color2" then
+ self:SetValue(val)
- self.OnValueChanged(self.vector * 1)
+ self.OnValueChanged(self.vector * 1)
+ end
end
- end):SetImage(pace.MiscIcons.paste)
+ end) pnl:SetImage(pace.MiscIcons.paste) pnl:SetTooltip(pace.clipboardtooltip)
menu:AddSpacer()
menu:AddOption(L"reset", function()
if pace.current_part and pace.current_part.DefaultVars[self.CurrentKey] then
@@ -1404,6 +2696,7 @@ do -- vector
end,
0.25
)
+
end
do -- number
@@ -1427,6 +2720,7 @@ do -- number
end
function PANEL:OnCursorMoved()
+ if self.used_by_proxy then self:SetCursor("no") return end
self:SetCursor("sizens")
end
@@ -1528,6 +2822,25 @@ do -- boolean
self.OnValueChanged(b)
self.lbl:SetText(L(tostring(b)))
end
+ chck.DoRightClick = function()
+ local menu = DermaMenu()
+ menu:SetPos(input.GetCursorPos())
+ if self.user_proxies then
+ for _,part in pairs(self.user_proxies) do
+ menu:AddOption("jump to " .. tostring(part), function()
+ pace.GoToPart(part)
+ end):SetImage("icon16/arrow_turn_right.png")
+ end
+ end
+ menu:AddOption(L"reset", function()
+ if pace.current_part and (pace.current_part.DefaultVars[self.CurrentKey] ~= nil) then
+ local val = pac.CopyValue(pace.current_part.DefaultVars[self.CurrentKey])
+ self:SetValue(val)
+ self.OnValueChanged(val)
+ end
+ end):SetImage(pace.MiscIcons.clear)
+ menu:MakePopup()
+ end
self.chck = chck
local lbl = vgui.Create("DLabel", self)
@@ -1544,6 +2857,11 @@ do -- boolean
self.chck:Toggle()
self.chck:Toggle()
self.lbl:SetText(L(tostring(b)))
+ if self.used_by_proxy then
+ if pace.special_property_text_color then
+ self.lbl:SetTextColor(pace.special_property_text_color)
+ end
+ end
self.during_change = false
end
@@ -1563,7 +2881,223 @@ do -- boolean
self.lbl:CenterVertical()
local w,h = self:GetParent():GetSize()
self:SetSize(w-2,h)
+ self.lbl:SetSize(w-h-2,h)
end
pace.RegisterPanel(PANEL)
end
+
+
+local tree_search_excluded_vars = {
+ ["ParentUID"] = true,
+ ["UniqueID"] = true,
+ ["ModelTracker"] = true,
+ ["ClassTracker"] = true,
+ ["LoadVmt"] = true
+}
+
+function pace.OpenTreeSearch()
+ --[[if GetConVar("pac_tree_lazymode"):GetBool() then
+ timer.Simple(0, function()
+ for i,v in pairs(pac.GetLocalParts()) do
+ v.no_populate = false
+ v.dormant_node = false
+ end
+ pace.RefreshTree(true)
+ end)
+ end]]
+ if pace.tree_search_open then return end
+ pace.Editor.y_offset = 24
+ pace.tree_search_open = true
+ pace.tree_search_match_index = 0
+ pace.tree_search_matches = {}
+ local resulting_part
+ local search_term = "friend"
+ local matched_property
+ local matches = {}
+
+ local base = vgui.Create("DFrame")
+ pace.tree_searcher = base
+ local edit = vgui.Create("DTextEntry", base)
+ local patterns = vgui.Create("DButton", base)
+ local search_button = vgui.Create("DButton", base)
+ local range_label = vgui.Create("DLabel", base)
+ local close_button = vgui.Create("DButton", base)
+ local case_box = vgui.Create("DButton", base)
+
+ case_box:SetText("Aa")
+ case_box:SetPos(325,2)
+ case_box:SetSize(25,20)
+ case_box:SetTooltip("case sensitive")
+ case_box:SetColor(Color(150,150,150))
+ case_box:SetFont("DermaDefaultBold")
+
+ patterns:SetText("^[abc]")
+ patterns:SetPos(490,2)
+ patterns:SetSize(40,20)
+ patterns:SetTooltip("use Lua patterns")
+ patterns:SetColor(Color(150,150,150))
+ patterns:SetFont("DermaDefaultBold")
+
+ function case_box:DoClick()
+ self.on = not self.on
+ if self.on then
+ self:SetColor(Color(0,0,0))
+ else
+ self:SetColor(Color(150,150,150))
+ end
+ end
+
+ function patterns:DoClick()
+ self.on = not self.on
+ if self.on then
+ self:SetColor(Color(0,0,0))
+ else
+ self:SetColor(Color(150,150,150))
+ end
+ end
+
+
+ local function select_match()
+ if table.IsEmpty(pace.tree_search_matches) then range_label:SetText("0 / 0") return end
+ if not pace.tree_search_matches[pace.tree_search_match_index] then return end
+
+ resulting_part = pace.tree_search_matches[pace.tree_search_match_index].part_matched
+ matched_property = pace.tree_search_matches[pace.tree_search_match_index].key_matched
+ if resulting_part ~= pace.current_part then pace.OnPartSelected(resulting_part, true) end
+ local parent = resulting_part:GetParent()
+ while IsValid(parent) and (parent:GetParent() ~= parent) do
+ parent.pace_tree_node:SetExpanded(true)
+ parent = parent:GetParent()
+ if parent:IsValid() then
+ parent.pace_tree_node:SetExpanded(true)
+ end
+ end
+ --pace.RefreshTree()
+ pace.FlashProperty(resulting_part, matched_property, false)
+ end
+
+ function base.OnRemove()
+ pace.tree_search_open = false
+ if not IsValid(pace.Editor) then return end
+ pace.Editor.y_offset = 0
+ end
+
+ function base.Think()
+ if not IsValid(pace.Editor) then base:Remove() return end
+ if not pace.Focused then base:Remove() end
+ base:SetX(pace.Editor:GetX())
+ base:SetWide(pace.Editor:GetWide())
+ end
+ function base.Paint(_,w,h)
+ surface.SetDrawColor(Color(255,255,255))
+ surface.DrawRect(0,0,w,h)
+ end
+ base:SetDraggable(false)
+ base:SetX(pace.Editor:GetX())
+ base:ShowCloseButton(false)
+
+ close_button:SetSize(40,20)
+ close_button:SetPos(450,2)
+ close_button:SetText("close")
+ function close_button.DoClick()
+ base:Remove()
+ end
+
+ local fwd = vgui.Create("DButton", base)
+ local bck = vgui.Create("DButton", base)
+
+ local function perform_search()
+ local case_sensitive = case_box.on
+ matches = {}
+ pace.tree_search_matches = {}
+ search_term = edit:GetText()
+ local nopatterns = patterns.on
+ if not case_sensitive then search_term = string.lower(search_term) end
+ for _,part in pairs(pac.GetLocalParts()) do
+ if (string.find(part.UniqueID, string.sub(search_term,2,#search_term-1)) or string.find(part.UniqueID, search_term)) and (#search_term > 8) then
+ table.insert(matches, #matches + 1, {part_matched = part, key_matched = "UniqueID"})
+ table.insert(pace.tree_search_matches, #matches, {part_matched = part, key_matched = "UniqueID"})
+ end
+
+ for k,v in pairs(part:GetProperties()) do
+ local value = v.get(part)
+
+ if (type(value) ~= "number" and type(value) ~= "string") or tree_search_excluded_vars[v.key] then continue end
+
+ value = tostring(value)
+ if not case_sensitive then value = string.lower(value) end
+
+
+ if string.find(case_sensitive and v.key or string.lower(v.key), search_term) or (string.find(value, search_term,1, not nopatterns)) then
+ if v.key == "Name" and part.Name == "" then continue end
+ table.insert(matches, #matches + 1, {part_matched = part, key_matched = v.key})
+ table.insert(pace.tree_search_matches, #matches, {part_matched = part, key_matched = v.key})
+ end
+ end
+ end
+ table.sort(pace.tree_search_matches, function(a, b)
+ if not IsValid(a.part_matched.pace_tree_node) then return false end
+ if not IsValid(b.part_matched.pace_tree_node) then return false end
+ return select(2, a.part_matched.pace_tree_node:LocalToScreen()) < select(2, b.part_matched.pace_tree_node:LocalToScreen())
+ end)
+ if table.IsEmpty(matches) then range_label:SetText("0 / 0") else pace.tree_search_match_index = 1 end
+ range_label:SetText(pace.tree_search_match_index .. " / " .. #pace.tree_search_matches)
+ end
+
+ base:SetSize(pace.Editor:GetWide(),24)
+ edit:SetSize(290,20)
+ edit:SetPos(0,2)
+ base:MakePopup()
+ edit:RequestFocus()
+ edit:SetUpdateOnType(true)
+ edit.previous_search = ""
+
+ range_label:SetSize(50,20)
+ range_label:SetPos(295,2)
+ range_label:SetText("0 / 0")
+ range_label:SetTextColor(Color(0,0,0))
+
+ fwd:SetSize(25,20)
+ fwd:SetPos(375,2)
+ fwd:SetText(">")
+ function fwd.DoClick()
+ if table.IsEmpty(pace.tree_search_matches) then range_label:SetText("0 / 0") return end
+ pace.tree_search_match_index = (pace.tree_search_match_index % math.max(#matches,1)) + 1
+ range_label:SetText(pace.tree_search_match_index .. " / " .. #pace.tree_search_matches)
+ select_match()
+ end
+
+ search_button:SetSize(50,20)
+ search_button:SetPos(400,2)
+ search_button:SetText("search")
+ function search_button.DoClick()
+ perform_search()
+ select_match()
+ end
+
+ bck:SetSize(25,20)
+ bck:SetPos(350,2)
+ bck:SetText("<")
+ function bck.DoClick()
+ if table.IsEmpty(pace.tree_search_matches) then range_label:SetText("0 / 0") return end
+ pace.tree_search_match_index = ((pace.tree_search_match_index - 2 + #matches) % math.max(#matches,1)) + 1
+ range_label:SetText(pace.tree_search_match_index .. " / " .. #pace.tree_search_matches)
+ select_match()
+ end
+
+ function edit.OnEnter()
+ if edit.previous_search ~= edit:GetText() then
+ perform_search()
+ edit.previous_search = edit:GetText()
+ elseif not table.IsEmpty(pace.tree_search_matches) then
+ fwd:DoClick()
+ else
+ perform_search()
+ end
+ select_match()
+
+ timer.Simple(0.1,function() edit:RequestFocus() end)
+ end
+
+end
\ No newline at end of file
diff --git a/lua/pac3/editor/client/panels/tree.lua b/lua/pac3/editor/client/panels/tree.lua
index eb3d91eab..1e6fe5fd1 100644
--- a/lua/pac3/editor/client/panels/tree.lua
+++ b/lua/pac3/editor/client/panels/tree.lua
@@ -1,3 +1,4 @@
+CreateClientConVar("pac_editor_scale","1", true, false)
local L = pace.LanguageString
local PANEL = {}
@@ -8,7 +9,7 @@ PANEL.Base = "pac_dtree"
function PANEL:Init()
pace.pac_dtree.Init(self)
- self:SetLineHeight(18)
+ self:SetLineHeight(18 * GetConVar("pac_editor_scale"):GetFloat())
self:SetIndentSize(10)
self.parts = {}
@@ -19,6 +20,7 @@ function PANEL:Init()
end
do
+
local function get_added_nodes(self)
local added_nodes = {}
for i,v in ipairs(self.added_nodes) do
@@ -55,64 +57,66 @@ do
function PANEL:Think(...)
if not pace.current_part:IsValid() then return end
- if
- pace.current_part.pace_tree_node and
- pace.current_part.pace_tree_node:IsValid() and not
- (
- pace.BusyWithProperties:IsValid() or
- pace.ActiveSpecialPanel:IsValid() or
- pace.editing_viewmodel or
- pace.editing_hands or
- pace.properties.search:HasFocus()
- ) and
- not gui.IsConsoleVisible()
- then
- if input.IsKeyDown(KEY_LEFT) then
- pace.Call("VariableChanged", pace.current_part, "EditorExpand", false)
- elseif input.IsKeyDown(KEY_RIGHT) then
- pace.Call("VariableChanged", pace.current_part, "EditorExpand", true)
- end
+ if GetConVar("pac_editor_shortcuts_legacy_mode"):GetBool() then
+ if
+ pace.current_part.pace_tree_node and
+ pace.current_part.pace_tree_node:IsValid() and not
+ (
+ pace.BusyWithProperties:IsValid() or
+ pace.ActiveSpecialPanel:IsValid() or
+ pace.editing_viewmodel or
+ pace.editing_hands or
+ pace.properties.search:HasFocus()
+ ) and
+ not gui.IsConsoleVisible()
+ then
+ if input.IsKeyDown(KEY_LEFT) then
+ pace.Call("VariableChanged", pace.current_part, "EditorExpand", false)
+ elseif input.IsKeyDown(KEY_RIGHT) then
+ pace.Call("VariableChanged", pace.current_part, "EditorExpand", true)
+ end
- if input.IsKeyDown(KEY_UP) or input.IsKeyDown(KEY_PAGEUP) then
- local added_nodes = get_added_nodes(self)
- local offset = input.IsKeyDown(KEY_PAGEUP) and 10 or 1
- if not self.scrolled_up or self.scrolled_up < os.clock() then
- for i,v in ipairs(added_nodes) do
- if v == pace.current_part.pace_tree_node then
- local node = added_nodes[i - offset] or added_nodes[1]
- if node then
- node:DoClick()
- scroll_to_node(self, node)
- break
+ if input.IsKeyDown(KEY_UP) or input.IsKeyDown(KEY_PAGEUP) then
+ local added_nodes = get_added_nodes(self)
+ local offset = input.IsKeyDown(KEY_PAGEUP) and 10 or 1
+ if not self.scrolled_up or self.scrolled_up < os.clock() then
+ for i,v in ipairs(added_nodes) do
+ if v == pace.current_part.pace_tree_node then
+ local node = added_nodes[i - offset] or added_nodes[1]
+ if node then
+ node:DoClick()
+ scroll_to_node(self, node)
+ break
+ end
end
end
- end
- self.scrolled_up = self.scrolled_up or os.clock() + 0.4
+ self.scrolled_up = self.scrolled_up or os.clock() + 0.4
+ end
+ else
+ self.scrolled_up = nil
end
- else
- self.scrolled_up = nil
- end
- if input.IsKeyDown(KEY_DOWN) or input.IsKeyDown(KEY_PAGEDOWN) then
- local added_nodes = get_added_nodes(self)
- local offset = input.IsKeyDown(KEY_PAGEDOWN) and 10 or 1
- if not self.scrolled_down or self.scrolled_down < os.clock() then
- for i,v in ipairs(added_nodes) do
- if v == pace.current_part.pace_tree_node then
- local node = added_nodes[i + offset] or added_nodes[#added_nodes]
- if node then
- node:DoClick()
- --scroll_to_node(self, node)
- break
+ if input.IsKeyDown(KEY_DOWN) or input.IsKeyDown(KEY_PAGEDOWN) then
+ local added_nodes = get_added_nodes(self)
+ local offset = input.IsKeyDown(KEY_PAGEDOWN) and 10 or 1
+ if not self.scrolled_down or self.scrolled_down < os.clock() then
+ for i,v in ipairs(added_nodes) do
+ if v == pace.current_part.pace_tree_node then
+ local node = added_nodes[i + offset] or added_nodes[#added_nodes]
+ if node then
+ node:DoClick()
+ --scroll_to_node(self, node)
+ break
+ end
end
end
- end
- self.scrolled_down = self.scrolled_down or os.clock() + 0.4
+ self.scrolled_down = self.scrolled_down or os.clock() + 0.4
+ end
+ else
+ self.scrolled_down = nil
end
- else
- self.scrolled_down = nil
end
end
@@ -156,15 +160,15 @@ do
if not node.Icon.event_icon then
local pnl = vgui.Create("DImage", node.Icon)
pnl:SetImage("icon16/clock_red.png")
- pnl:SetSize(8, 8)
- pnl:SetPos(8, 8)
+ pnl:SetSize(8*(1 + 0.5*(GetConVar("pac_editor_scale"):GetFloat()-1)), 8*(1 + 0.5*(GetConVar("pac_editor_scale"):GetFloat()-1)))
+ pnl:SetPos(8*(1 + 0.5*(GetConVar("pac_editor_scale"):GetFloat()-1)), 8*(1 + 0.5*(GetConVar("pac_editor_scale"):GetFloat()-1)))
pnl:SetVisible(false)
node.Icon.event_icon = pnl
end
node.Icon.event_icon:SetVisible(true)
else
- if node.Icon.event_icon then
+ if node.Icon.event_icon and not node.Icon.event_icon_alt then
node.Icon.event_icon:SetVisible(false)
end
end
@@ -183,8 +187,83 @@ do
end
end
end
+
+ local function DoScrollControl(self, action)
+ pace.BulkSelectKey = input.GetKeyCode(GetConVar("pac_bulk_select_key"):GetString())
+ if
+ pace.current_part.pace_tree_node and
+ pace.current_part.pace_tree_node:IsValid() and not
+ (
+ pace.BusyWithProperties:IsValid() or
+ pace.ActiveSpecialPanel:IsValid() or
+ pace.editing_viewmodel or
+ pace.editing_hands or
+ pace.properties.search:HasFocus()
+ ) and
+ not gui.IsConsoleVisible()
+ then
+
+ if action == "editor_node_collapse" then
+ pace.Call("VariableChanged", pace.current_part, "EditorExpand", false)
+ elseif action == "editor_node_expand" then
+ pace.Call("VariableChanged", pace.current_part, "EditorExpand", true)
+ end
+
+ if action == "editor_up" or action == "editor_pageup" then
+ local added_nodes = get_added_nodes(self)
+ local offset = action == "editor_pageup" and 10 or 1
+ if not self.scrolled_up or self.scrolled_up < os.clock() then
+ for i,v in ipairs(added_nodes) do
+ if v == pace.current_part.pace_tree_node then
+ local node = added_nodes[i - offset] or added_nodes[1]
+ if node then
+ node:DoClick()
+ scroll_to_node(self, node)
+ if input.IsKeyDown(pace.BulkSelectKey) then pace.DoBulkSelect(node.part, true) end
+ break
+ end
+ end
+ end
+
+ self.scrolled_up = self.scrolled_up or os.clock() + 0.4
+ end
+ else
+ self.scrolled_up = nil
+ end
+
+ if action == "editor_down" or action == "editor_pagedown" then
+ local added_nodes = get_added_nodes(self)
+ local offset = action == "editor_pagedown" and 10 or 1
+ if not self.scrolled_down or self.scrolled_down < os.clock() then
+ for i,v in ipairs(added_nodes) do
+ if v == pace.current_part.pace_tree_node then
+ local node = added_nodes[i + offset] or added_nodes[#added_nodes]
+ if node then
+ node:DoClick()
+ if input.IsKeyDown(pace.BulkSelectKey) then pace.DoBulkSelect(node.part, true) end
+ --scroll_to_node(self, node)
+ break
+ end
+ end
+ end
+
+ self.scrolled_down = self.scrolled_down or os.clock() + 0.4
+ end
+ else
+ self.scrolled_down = nil
+ end
+ end
+ end
+
+ function pace.DoScrollControls(action)
+ DoScrollControl(pace.tree, action)
+ end
+
end
+
+
+
function PANEL:OnMouseReleased(mc)
if mc == MOUSE_RIGHT then
pace.Call("PartMenu")
@@ -192,7 +271,7 @@ function PANEL:OnMouseReleased(mc)
end
function PANEL:SetModel(path)
- if not file.Exists(path, "GAME") then
+ if not file.Exists(path or "", "GAME") then
path = player_manager.TranslatePlayerModel(path)
if not file.Exists(path, "GAME") then
print(path, "is invalid")
@@ -229,6 +308,7 @@ local function install_drag(node)
if self.part and self.part:IsValid() and self.part:GetParent() ~= child.part then
pace.RecordUndoHistory()
self.part:SetParent(child.part)
+ pace.RefreshEvents()
pace.RecordUndoHistory()
end
elseif self.part and self.part:IsValid() then
@@ -237,6 +317,7 @@ local function install_drag(node)
local group = pac.CreatePart("group", self.part:GetPlayerOwner())
group:SetEditorExpand(true)
self.part:SetParent(group)
+ pace.RefreshEvents()
pace.RecordUndoHistory()
pace.TrySelectPart()
@@ -264,6 +345,7 @@ local function install_drag(node)
if self.part and self.part:IsValid() and child.part:GetParent() ~= self.part then
pace.RecordUndoHistory()
child.part:SetParent(self.part)
+ pace.RefreshEvents()
pace.RecordUndoHistory()
end
end
@@ -298,6 +380,25 @@ local function install_expand(node)
node.part:CallRecursive('SetEditorExpand', true)
pace.RefreshTree(true)
end):SetImage('icon16/arrow_down.png')
+
+ menu:AddSpacer()
+
+ local menu1, pnl = menu:AddSubMenu(L"double click actions") pnl:SetIcon("icon16/cursor.png")
+ menu1.GetDeleteSelf = function() return false end
+ local menu2, pnl = menu1:AddSubMenu(L"generic") pnl:SetIcon("icon16/world.png")
+ menu2.GetDeleteSelf = function() return false end
+ menu2:AddOption("expand / collapse", function() RunConsoleCommand("pac_doubleclick_action", "expand") end):SetImage('icon16/arrow_down.png')
+ menu2:AddOption("rename", function() RunConsoleCommand("pac_doubleclick_action", "rename") end):SetImage('icon16/text_align_center.png')
+ menu2:AddOption("write notes", function() RunConsoleCommand("pac_doubleclick_action", "notes") end):SetImage('icon16/page_white_edit.png')
+ menu2:AddOption("show / hide", function() RunConsoleCommand("pac_doubleclick_action", "showhide") end):SetImage('icon16/clock_red.png')
+ menu2:AddOption("only when specifed actions exist", function() RunConsoleCommand("pac_doubleclick_action", "specific_only") end):SetImage('icon16/application_xp_terminal.png')
+ menu2:AddOption("none", function() RunConsoleCommand("pac_doubleclick_action", "none") end):SetImage('icon16/collision_off.png')
+ local menu2, pnl = menu1:AddSubMenu(L"specific") pnl:SetIcon("icon16/application_xp_terminal.png")
+ menu2.GetDeleteSelf = function() return false end
+ menu2:AddOption("use generic actions only", function() RunConsoleCommand("pac_doubleclick_action_specified", "0") end):SetImage('icon16/world.png')
+ menu2:AddOption("use specific actions when available", function() RunConsoleCommand("pac_doubleclick_action_specified", "1") end):SetImage('icon16/cog.png')
+ menu2:AddOption("use even more specific actions (events)", function() RunConsoleCommand("pac_doubleclick_action_specified", "2") end):SetImage('icon16/clock.png')
+
end
end
end
@@ -352,7 +453,7 @@ function PANEL:AddNode(...)
local add_button = node:Add("DImageButton")
add_button:SetImage(pace.MiscIcons.new)
- add_button:SetSize(16, 16)
+ add_button:SetSize(16*GetConVar("pac_editor_scale"):GetFloat(), 16*GetConVar("pac_editor_scale"):GetFloat())
add_button:SetVisible(false)
add_button.DoClick = function() add_parts_menu(node) pace.Call("PartSelected", node.part) end
add_button.DoRightClick = function() node:DoRightClick() end
@@ -419,7 +520,8 @@ function PANEL:PopulateParts(node, parts, children)
fix_folder_funcs(part_node)
- if part.Description then part_node:SetTooltip(L(part.Description)) end
+ if part.Description then part_node:SetTooltip(L(part.Description)) end --ok but have we ever had any Description other than "right click to add parts"?
+ if part.Notes ~= "" then part_node.Label:SetTooltip(part.Notes) end --idk if anyone uses Notes but tooltips are good if they have something on them. It can easily be overridden by other code anyway.
part.pace_tree_node = part_node
part_node.part = part
@@ -454,6 +556,7 @@ function PANEL:PopulateParts(node, parts, children)
elseif isstring(part.Icon) then
part_node.Icon:SetImage(part.Icon)
end
+ part_node.Icon:SetSize(16 * GetConVar("pac_editor_scale"):GetFloat(),16 * GetConVar("pac_editor_scale"):GetFloat())
self:PopulateParts(part_node, part:GetChildren(), true)
@@ -499,8 +602,8 @@ end
function PANEL:Populate(reset)
- self:SetLineHeight(18)
- self:SetIndentSize(2)
+ self:SetLineHeight(18 * (1 + (GetConVar("pac_editor_scale"):GetFloat()-1)))
+ self:SetIndentSize(10)
for key, node in pairs(self.parts) do
if reset or (not node.part or not node.part:IsValid()) then
@@ -530,12 +633,12 @@ local function remove_node(part)
part.pace_tree_node:GetRoot().m_pSelectedItem = nil
part.pace_tree_node:Remove()
pace.RefreshTree()
+
end
end
pac.AddHook("pac_OnPartRemove", "pace_remove_tree_nodes", remove_node)
-
local last_refresh = 0
local function refresh(part)
if last_refresh > SysTime() then return end
@@ -564,7 +667,36 @@ pac.AddHook("pace_OnVariableChanged", "pace_create_tree_nodes", function(part, k
end
end)
+pace.allowed_event_refresh = 0
+
+
+function pace.RefreshEvents()
+ --spam preventer, (load parts' initializes gets called)
+ if pace.allowed_event_refresh > CurTime() then return else pace.allowed_event_refresh = CurTime() + 0.1 end
+
+ local events = {}
+ for _, part in pairs(pac.GetLocalParts()) do
+ if part.ClassName == "event" then
+ events[part] = part
+ end
+ end
+ local no_events = table.Count(events) == 0
+
+ for _, child in pairs(pac.GetLocalParts()) do
+ child.active_events = {}
+ child.active_events_ref_count = 0
+ if not no_events then
+ for _,event in pairs(events) do
+ event:OnThink()
+ end
+ end
+ child:CallRecursive("CalcShowHide", false)
+ end
+
+end
+
function pace.RefreshTree(reset)
+ --print("pace.RefreshTree("..tostring(reset)..")")
if pace.tree:IsValid() then
timer.Create("pace_refresh_tree", 0.01, 1, function()
if pace.tree:IsValid() then
@@ -575,6 +707,7 @@ function pace.RefreshTree(reset)
end
end)
end
+
end
if Entity(1):IsPlayer() and not PAC_RESTART and not VLL2_FILEDEF then
diff --git a/lua/pac3/editor/client/parts.lua b/lua/pac3/editor/client/parts.lua
index 65fad2a3d..8d8dbd265 100644
--- a/lua/pac3/editor/client/parts.lua
+++ b/lua/pac3/editor/client/parts.lua
@@ -1,4 +1,55 @@
+--include("pac3/editor/client/panels/properties.lua")
+include("popups_part_tutorials.lua")
+
local L = pace.LanguageString
+pace.BulkSelectList = {}
+pace.BulkSelectUIDs = {}
+pace.BulkSelectClipboard = {}
+local refresh_halo_hook = true
+pace.operations_all_operations = {"wear", "copy", "paste", "cut", "paste_properties", "clone", "spacer", "registered_parts", "save", "load", "remove", "bulk_select", "bulk_apply_properties", "partsize_info", "hide_editor", "expand_all", "collapse_all", "copy_uid", "help_part_info", "reorder_movables", "arraying_menu", "criteria_process", "bulk_morph", "view_goto", "view_lockon"}
+
+pace.operations_default = {"help_part_info", "wear", "copy", "paste", "cut", "paste_properties", "clone", "spacer", "registered_parts", "spacer", "bulk_select", "bulk_apply_properties", "spacer", "save", "load", "spacer", "remove"}
+pace.operations_legacy = {"wear", "copy", "paste", "cut", "paste_properties", "clone", "spacer", "registered_parts", "spacer", "save", "load", "spacer", "remove"}
+
+pace.operations_experimental = {"help_part_info", "wear", "copy", "paste", "cut", "paste_properties", "clone", "bulk_select", "spacer", "registered_parts", "spacer", "bulk_apply_properties", "partsize_info", "copy_uid", "spacer", "save", "load", "spacer", "remove"}
+pace.operations_bulk_poweruser = {"bulk_select", "clone", "registered_parts", "spacer", "copy", "paste", "cut", "spacer", "wear", "save", "load", "partsize_info"}
+
+if not file.Exists("pac3_config/pac_editor_partmenu_layouts.txt", "DATA") then
+ pace.operations_order = pace.operations_default
+end
+
+local hover_color = CreateConVar( "pac_hover_color", "255 255 255", FCVAR_ARCHIVE, "R G B value of the highlighting when hovering over pac3 parts, there are also special options: none, ocean, funky, rave, rainbow")
+CreateConVar( "pac_hover_pulserate", 20, FCVAR_ARCHIVE, "pulse rate of the highlighting when hovering over pac3 parts")
+CreateConVar( "pac_hover_halo_limit", 100, FCVAR_ARCHIVE, "max number of parts before hovering over pac3 parts stops computing to avoid lag")
+
+CreateConVar("pac_bulk_select_key", "ctrl", FCVAR_ARCHIVE, "Button to hold to use bulk select")
+CreateConVar("pac_bulk_select_halo_mode", 1, FCVAR_ARCHIVE, "Halo Highlight mode.\n0 is no highlighting\n1 is passive\n2 is when the same key as bulk select is pressed\n3 is when control key pressed\n4 is when shift key is pressed.")
+local bulk_select_subsume = CreateConVar("pac_bulk_select_subsume", "1", FCVAR_ARCHIVE, "Whether bulk-selecting a part implicitly deselects its children since they are covered by the parent already.\nWhile it can provide a clearer view of what's being selected globally which simplifies broad operations like deleting, moving and copying, it prevents targeted operations on nested parts like bulk property editing.")
+local bulk_select_deselect = CreateConVar("pac_bulk_select_deselect", "1", FCVAR_ARCHIVE, "Whether selecting a part without holding bulk select key will deselect the bulk selected parts")
+local bulkselect_cursortext = CreateConVar("pac_bulk_select_cursor_info", "1", FCVAR_ARCHIVE, "Whether to draw some info next to your cursor when there is a bulk selection")
+
+CreateConVar("pac_copilot_partsearch_depth", -1, FCVAR_ARCHIVE, "amount of copiloting in the searchable part menu\n-1:none\n0:auto-focus on the text edit for events\n1:bring up a list of clickable event types\nother parts aren't supported yet")
+CreateConVar("pac_copilot_make_popup_when_selecting_event", 1, FCVAR_ARCHIVE, "whether to create a popup so you can read what an event does")
+CreateConVar("pac_copilot_open_asset_browser_when_creating_part", 0, FCVAR_ARCHIVE, "whether to open the asset browser for models, materials, or sounds")
+CreateConVar("pac_copilot_force_preview_cameras", 1, FCVAR_ARCHIVE, "whether to force the editor camera off when creating a camera part")
+CreateConVar("pac_copilot_auto_setup_command_events", 0, FCVAR_ARCHIVE, "whether to automatically setup a command event if the name you type doesn't match an existing event. we'll assume you want a command event name.\nif this is set to 0, it will only auto-setup in such a case if you already have such a command event actively present in your events or waiting to be activated by your command parts")
+CreateConVar("pac_copilot_auto_focus_main_property_when_creating_part", 1, FCVAR_ARCHIVE, "whether to automatically focus on the main property that defines a part, such as the event's event type, the text's text, the proxy's expression or the command's string.")
+
+--the necessary properties we always edit for certain parts
+--others might be opened with the asset browser so this is not the full list
+--should be a minimal list because we don't want to get too much in the way of routine editing
+local star_properties = {
+ ["event"] = "Event",
+ ["proxy"] = "Expression",
+ ["text"] = "Text",
+ ["command"] = "String",
+ ["animation"] = "SequenceName",
+ ["flex"] = "Flex",
+ ["bone3"] = "Bone",
+ ["poseparameter"] = "PoseParameter",
+ ["damage_zone"] = "Damage",
+ ["hitscan"] = "Damage"
+}
-- load only when hovered above
local function add_expensive_submenu_load(pnl, callback)
@@ -10,7 +61,136 @@ local function add_expensive_submenu_load(pnl, callback)
end
end
+
+local function BulkSelectRefreshFadedNodes(part_trace)
+ if refresh_halo_hook then return end
+ if part_trace then
+ for _,v in ipairs(part_trace:GetRootPart():GetChildrenList()) do
+ if IsValid(v.pace_tree_node) then
+ v.pace_tree_node:SetAlpha( 255 )
+ end
+
+ end
+ end
+
+ for _,v in ipairs(pace.BulkSelectList) do
+ if not v:IsValid() then table.RemoveByValue(pace.BulkSelectList, v)
+ elseif IsValid(v.pace_tree_node) then
+ if bulk_select_subsume:GetBool() then
+ v.pace_tree_node:SetAlpha( 150 )
+ else
+ v.pace_tree_node:SetAlpha( 255 )
+ end
+ end
+ end
+end
+
+local function RebuildBulkHighlight()
+ local parts_tbl = {}
+ local ents_tbl = {}
+ local hover_tbl = {}
+ local ent = {}
+
+ --get potential entities and part-children from each parent in the bulk list
+ for _,v in pairs(pace.BulkSelectList) do --this will get parts
+
+ if (v == v:GetRootPart()) then --if this is the root part, send the entity
+ table.insert(ents_tbl,v:GetRootPart():GetOwner())
+ table.insert(parts_tbl,v)
+ else
+ table.insert(parts_tbl,v)
+ end
+
+ for _,child in ipairs(v:GetChildrenList()) do --now do its children
+ table.insert(parts_tbl,child)
+ end
+ end
+
+ --check what parts are candidates we can give to halo
+ for _,v in ipairs(parts_tbl) do
+ local can_add = false
+ if (v.ClassName == "model" or v.ClassName == "model2") then
+ can_add = true
+ end
+ if (v.ClassName == "group") or (v.Hide == true) or (v.Size == 0) or (v.Alpha == 0) or (v:IsHidden()) then
+ can_add = false
+ end
+ if can_add then
+ table.insert(hover_tbl, v:GetOwner())
+ end
+ end
+
+ table.Add(hover_tbl,ents_tbl)
+ --TestPrintTable(hover_tbl, "hover_tbl")
+
+ last_bulk_select_tbl = hover_tbl
+end
+
+local function TestPrintTable(tbl, tbl_name)
+ MsgC(Color(200,255,200), "TABLE CONTENTS:" .. tbl_name .. " = {\n")
+ for _,v in pairs(tbl) do
+ MsgC(Color(200,255,200), "\t", tostring(v), ", \n")
+ end
+ MsgC(Color(200,255,200), "}\n")
+end
+
+local function DrawHaloHighlight(tbl)
+ if (type(tbl) ~= "table") then return end
+ if not pace.Active then
+ pac.RemoveHook("PreDrawHalos", "BulkSelectHighlights")
+ end
+ if pace.camera_orthographic then return end
+
+ --Find out the color and apply the halo
+ local color_string = GetConVar("pac_hover_color"):GetString()
+ local pulse_rate = math.min(math.abs(GetConVar("pac_hover_pulserate"):GetFloat()), 100)
+ local pulse = math.sin(SysTime() * pulse_rate) * 0.5 + 0.5
+ if pulse_rate == 0 then pulse = 1 end
+ local pulseamount
+
+ local halo_color = Color(255,255,255)
+
+ if color_string == "rave" then
+ halo_color = Color(255*((0.33 + SysTime() * pulse_rate/20)%1), 255*((0.66 + SysTime() * pulse_rate/20)%1), 255*((SysTime() * pulse_rate/20)%1), 255)
+ pulseamount = 8
+ elseif color_string == "funky" then
+ halo_color = Color(255*((0.33 + SysTime() * pulse_rate/10)%1), 255*((0.2 + SysTime() * pulse_rate/15)%1), 255*((SysTime() * pulse_rate/15)%1), 255)
+ pulseamount = 5
+ elseif color_string == "ocean" then
+ halo_color = Color(0, 80 + 30*(pulse), 200 + 50*(pulse) * 0.5 + 0.5, 255)
+ pulseamount = 4
+ elseif color_string == "rainbow" then
+ --halo_color = Color(255*(0.5 + 0.5*math.sin(pac.RealTime * pulse_rate/20)),255*(0.5 + 0.5*-math.cos(pac.RealTime * pulse_rate/20)),255*(0.5 + 0.5*math.sin(1 + pac.RealTime * pulse_rate/20)), 255)
+ halo_color = HSVToColor(SysTime() * 360 * pulse_rate/20, 1, 1)
+ pulseamount = 4
+ elseif #string.Split(color_string, " ") == 3 then
+ halo_color_tbl = string.Split( color_string, " " )
+ for i,v in ipairs(halo_color_tbl) do
+ if not isnumber(tonumber(halo_color_tbl[i])) then halo_color_tbl[i] = 0 end
+ end
+ halo_color = Color(pulse*halo_color_tbl[1],pulse*halo_color_tbl[2],pulse*halo_color_tbl[3],255)
+ pulseamount = 4
+ else
+ halo_color = Color(255,255,255,255)
+ pulseamount = 2
+ end
+ --print("using", halo_color, "blurs=" .. 2, "amount=" .. pulseamount)
+
+ pac.haloex.Add(tbl, halo_color, 2, 2, pulseamount, true, true, pulseamount, 1, 1)
+ --haloex.Add( ents, color, blurx, blury, passes, add, ignorez, amount, spherical, shape )
+end
+
+local function ThinkBulkHighlight()
+ if table.IsEmpty(pace.BulkSelectList) or last_bulk_select_tbl == nil or table.IsEmpty(pac.GetLocalParts()) or (#pac.GetLocalParts() == 1) then
+ pac.RemoveHook("PreDrawHalos", "BulkSelectHighlights")
+ return
+ end
+ DrawHaloHighlight(last_bulk_select_tbl)
+end
+
+
function pace.WearParts(temp_wear_filter)
+
local allowed, reason = pac.CallHook("CanWearParts", pac.LocalPlayer)
if allowed == false then
@@ -98,13 +278,150 @@ function pace.OnCreatePart(class_name, name, mdl, no_parent)
end
pace.RefreshTree()
+ timer.Simple(0.3, function() BulkSelectRefreshFadedNodes() end)
+
+ if GetConVar("pac_copilot_open_asset_browser_when_creating_part"):GetBool() then
+ timer.Simple(0.5, function()
+ local self = nil
+ if class_name == "model2" then
+ self = pace.current_part.pace_properties["Model"]
+
+ pace.AssetBrowser(function(path)
+ if not part:IsValid() then return end
+ -- because we refresh the properties
+
+ if IsValid(self) and self.OnValueChanged then
+ self.OnValueChanged(path)
+ end
+
+ if pace.current_part.SetMaterials then
+ local model = pace.current_part:GetModel()
+ local part = pace.current_part
+ if part.pace_last_model and part.pace_last_model ~= model then
+ part:SetMaterials("")
+ end
+ part.pace_last_model = model
+ end
+
+ pace.PopulateProperties(pace.current_part)
+
+ for k,v in ipairs(pace.properties.List) do
+ if v.panel and v.panel.part == part and v.key == key then
+ self = v.panel
+ break
+ end
+ end
+
+ end, "models")
+ elseif class_name == "sound" or class_name == "sound2" then
+ if class_name == "sound"then
+ self = pace.current_part.pace_properties["Sound"]
+ elseif class_name == "sound2" then
+ self = pace.current_part.pace_properties["Path"]
+ end
+
+ pace.AssetBrowser(function(path)
+ if not self:IsValid() then return end
+
+ self:SetValue(path)
+ self.OnValueChanged(path)
+
+ end, "sound")
+ elseif pace.current_part.pace_properties["LoadVmt"] then
+ self = pace.current_part.pace_properties["LoadVmt"]
+ pace.AssetBrowser(function(path)
+ if not self:IsValid() then return end
+ path = string.gsub(string.StripExtension(path), "^materials/", "") or "error"
+ self:SetValue(path)
+ self.OnValueChanged(path)
+ pace.current_part:SetLoadVmt(path)
+ end, "materials")
+
+ end
+ end)
+
+ end
+ if class_name == "camera" and GetConVar("pac_copilot_force_preview_cameras"):GetBool() then
+ timer.Simple(0.2, function() pace.EnableView(false) end)
+ end
+ if GetConVar("pac_copilot_auto_focus_main_property_when_creating_part"):GetBool() then
+ if star_properties[part.ClassName] then
+ timer.Simple(0.2, function()
+
+ pace.FlashProperty(part, star_properties[part.ClassName], true)
+
+ end)
+ end
+ end
return part
end
+local last_span_select_part
+local last_select_was_span = false
+local last_direction
+
+pac.AddHook("VGUIMousePressed", "kecode_tracker", function(pnl, mc)
+ pace.last_mouse_code = mc
+end)
+
function pace.OnPartSelected(part, is_selecting)
- local parent = part:GetRootPart()
+ pace.delaybulkselect = pace.delaybulkselect or 0 --a time updated in shortcuts.lua to prevent common pac operations from triggering bulk selection
+ local bulk_key_pressed = input.IsKeyDown(input.GetKeyCode(GetConVar("pac_bulk_select_key"):GetString()))
+
+ if (not bulk_key_pressed) and bulk_select_deselect:GetBool() then
+ if pace.last_mouse_code == MOUSE_LEFT then pace.ClearBulkList(true) end
+ end
+
+ if RealTime() > pace.delaybulkselect and bulk_key_pressed and not input.IsKeyDown(input.GetKeyCode("v")) and not input.IsKeyDown(input.GetKeyCode("z")) and not input.IsKeyDown(input.GetKeyCode("y")) then
+ --jumping multi-select if holding shift + ctrl
+ if bulk_key_pressed and input.IsShiftDown() then
+ --ripped some local functions from tree.lua
+ local added_nodes = {}
+ for i,v in ipairs(pace.tree.added_nodes) do
+ if v.part and v:IsVisible() and v:IsExpanded() then
+ table.insert(added_nodes, v)
+ end
+ end
+
+ local startnodenumber = table.KeyFromValue( added_nodes, pace.current_part.pace_tree_node)
+ local endnodenumber = table.KeyFromValue( added_nodes, part.pace_tree_node)
+
+ if not startnodenumber or not endnodenumber then return end
+
+ table.sort(added_nodes, function(a, b) return select(2, a:LocalToScreen()) < select(2, b:LocalToScreen()) end)
+
+ local i = startnodenumber
+ local direction = math.Clamp(endnodenumber - startnodenumber,-1,1)
+ if direction == 0 then last_direction = direction return end
+ last_direction = last_direction or 0
+ if last_span_select_part == nil then last_span_select_part = part end
+
+ if last_select_was_span then
+ if last_direction == -direction then
+ pace.DoBulkSelect(pace.current_part, true)
+ end
+ if last_span_select_part == pace.current_part then
+ pace.DoBulkSelect(pace.current_part, true)
+ end
+ end
+ while (i ~= endnodenumber) do
+ pace.DoBulkSelect(added_nodes[i].part, true)
+ i = i + direction
+ end
+ pace.DoBulkSelect(part)
+ last_direction = direction
+ last_select_was_span = true
+ else
+ pace.DoBulkSelect(part)
+ last_select_was_span = false
+ end
+
+ else last_select_was_span = false end
+ last_span_select_part = part
+
+ local parent = part:GetRootPart()
if parent:IsValid() and (parent.OwnerName == "viewmodel" or parent.OwnerName == "hands") then
pace.editing_viewmodel = parent.OwnerName == "viewmodel"
pace.editing_hands = parent.OwnerName == "hands"
@@ -114,15 +431,15 @@ function pace.OnPartSelected(part, is_selecting)
end
pace.current_part = part
+ if pace.bypass_tree then return end
+
pace.PopulateProperties(part)
pace.mctrl.SetTarget(part)
pace.SetViewPart(part)
-
if pace.Editor:IsValid() then
pace.Editor:InvalidateLayout()
end
-
pace.SafeRemoveSpecialPanel()
if pace.tree:IsValid() then
@@ -134,13 +451,36 @@ function pace.OnPartSelected(part, is_selecting)
if not is_selecting then
pace.StopSelect()
end
+
+end
+
+pace.suppress_flashing_property = false
+
+function pace.FlashProperty(obj, key, edit)
+ if pace.suppress_flashing_property then return end
+ if not obj.flashing_property then
+ obj.flashing_property = true
+ timer.Simple(0.1, function()
+ if not obj.pace_properties[key] then return end
+ obj.pace_properties[key]:Flash()
+ pace.current_flashed_property = key
+ if edit then
+ obj.pace_properties[key]:RequestFocus()
+ if obj.pace_properties[key].EditText then
+ obj.pace_properties[key]:EditText()
+ end
+ end
+ end)
+ timer.Simple(0.3, function() obj.flashing_property = false end)
+ end
+
end
function pace.OnVariableChanged(obj, key, val, not_from_editor)
local valType = type(val)
- if valType == 'Vector' then
+ if valType == "Vector" then
val = Vector(val)
- elseif valType == 'Angle' then
+ elseif valType == "Angle" then
val = Angle(val)
end
@@ -241,12 +581,17 @@ function pace.GetRegisteredParts()
end
do -- menu
+
local trap
+ if not pace.Active or refresh_halo_hook then
+ pac.RemoveHook("PreDrawHalos", "BulkSelectHighlights")
+ end
+//@note registered parts
function pace.AddRegisteredPartsToMenu(menu, parent)
local partsToShow = {}
local clicked = false
- hook.Add('Think', menu, function()
+ pac.AddHook("Think", menu, function()
local ctrl = input.IsControlDown()
if clicked and not ctrl then
@@ -259,7 +604,7 @@ do -- menu
menu:SetDeleteSelf(not ctrl)
end)
- hook.Add('CloseDermaMenus', menu, function()
+ pac.AddHook("CloseDermaMenus", menu, function()
clicked = true
if input.IsControlDown() then
menu:SetVisible(true)
@@ -268,7 +613,7 @@ do -- menu
end)
local function add_part(menu, part)
- local newMenuEntry = menu:AddOption(L(part.FriendlyName or part.ClassName:Replace('_', ' ')), function()
+ local newMenuEntry = menu:AddOption(L(part.FriendlyName or part.ClassName:Replace("_", " ")), function()
pace.RecordUndoHistory()
pace.Call("CreatePart", part.ClassName, nil, nil, parent)
pace.RecordUndoHistory()
@@ -286,17 +631,40 @@ do -- menu
end
end
end
+ return newMenuEntry
end
local sortedTree = {}
-
+ local PartStructure = {}
+ local Groups = {}
+ local Parts = pac.GetRegisteredParts()
for _, part in pairs(pace.GetRegisteredParts()) do
- local group = part.Group or part.Groups or "other"
+ local class = part.ClassName
+ local groupname = "other"
+ local group = part.Group or part.Groups or "other"
+ --print(group)
if isstring(group) then
+ --MsgC(Color(0,255,0), "\t" .. group .. "\n")
+ groupname = group
group = {group}
+ else
+ --PrintTable(group)
+ Groups[groupname] = Groups[groupname] or {}
+ for i,v in ipairs(group) do
+ Groups[v] = Groups[v] or {}
+ Groups[v][class] = Groups[v][class] or class
+ end
end
+ Groups[groupname] = Groups[groupname] or group
+
+ Groups[groupname][class] = Groups[groupname][class] or class
+
+ --[[if isstring(group) then
+ group = {group}
+ end]]
+
for i, name in ipairs(group) do
if not sortedTree[name] then
sortedTree[name] = {}
@@ -315,51 +683,116 @@ do -- menu
end
end
+ --file.Write("pac_partgroups.txt", util.TableToKeyValues(Groups))
+
local other = sortedTree.other
sortedTree.other = nil
- for group, groupData in pairs(sortedTree) do
- local sub, pnl = menu:AddSubMenu(groupData.name, function()
- if groupData.group_class_name then
- pace.RecordUndoHistory()
- pace.Call("CreatePart", groupData.group_class_name, nil, nil, parent)
- pace.RecordUndoHistory()
+ if not file.Exists("pac3_config/pac_part_categories.txt", "DATA") then
+ for group, groupData in pairs(sortedTree) do
+ local sub, pnl = menu:AddSubMenu(groupData.name, function()
+ if groupData.group_class_name then
+ pace.RecordUndoHistory()
+ pace.Call("CreatePart", groupData.group_class_name, nil, nil, parent)
+ pace.RecordUndoHistory()
+ end
+ end)
+
+ sub.GetDeleteSelf = function() return false end
+
+ if groupData.icon then
+ pnl:SetImage(groupData.icon)
end
- end)
- sub.GetDeleteSelf = function() return false end
+ trap = false
+ table.sort(groupData.parts, function(a, b) return a.ClassName < b.ClassName end)
+ for i, part in ipairs(groupData.parts) do
+ add_part(sub, part)
+ end
- if groupData.icon then
- pnl:SetImage(groupData.icon)
- end
+ pac.AddHook("Think", sub, function()
+ local ctrl = input.IsControlDown()
- trap = false
- table.sort(groupData.parts, function(a, b) return a.ClassName < b.ClassName end)
- for i, part in ipairs(groupData.parts) do
- add_part(sub, part)
- end
+ if clicked and not ctrl then
+ sub:SetDeleteSelf(true)
+ RegisterDermaMenuForClose(sub)
+ CloseDermaMenus()
+ return
+ end
- hook.Add('Think', sub, function()
- local ctrl = input.IsControlDown()
+ sub:SetDeleteSelf(not ctrl)
+ end)
+
+ pac.AddHook("CloseDermaMenus", sub, function()
+ if input.IsControlDown() and trap then
+ trap = false
+ sub:SetVisible(true)
+ end
- if clicked and not ctrl then
- sub:SetDeleteSelf(true)
RegisterDermaMenuForClose(sub)
- CloseDermaMenus()
- return
- end
+ end)
+ end
+ else --custom part categories
+ pace.partgroups = pace.partgroups or util.KeyValuesToTable(file.Read("pac3_config/pac_part_categories.txt", "DATA"))
+ Groups = pace.partgroups
+ --group is the group name
+ --tbl is a shallow table with part class names
+ --PrintTable(Groups)
+ for group, tbl in pairs(Groups) do
- sub:SetDeleteSelf(not ctrl)
- end)
+ local sub, pnl = menu:AddSubMenu(group, function()
+ if Parts[group] then
+ if group == "entity" then
+ pace.RecordUndoHistory()
+ pace.Call("CreatePart", "entity2", nil, nil, parent)
+ pace.RecordUndoHistory()
+ elseif group == "model" then
+ pace.RecordUndoHistory()
+ pace.Call("CreatePart", "model2", nil, nil, parent)
+ pace.RecordUndoHistory()
+ else
+ pace.RecordUndoHistory()
+ pace.Call("CreatePart", group, nil, nil, parent)
+ pace.RecordUndoHistory()
+ end
+ end
+ end)
- hook.Add('CloseDermaMenus', sub, function()
- if input.IsControlDown() and trap then
- trap = false
- sub:SetVisible(true)
+--@note partmenu definer
+ sub.GetDeleteSelf = function() return false end
+
+ if tbl["icon"] then
+ --print(tbl["icon"])
+ if pace.MiscIcons[string.gsub(tbl["icon"], "pace.MiscIcons.", "")] then
+ pnl:SetImage(pace.MiscIcons[string.gsub(tbl["icon"], "pace.MiscIcons.", "")])
+ else
+ local img = string.gsub(tbl["icon"], ".png", "") --remove the png extension
+ img = string.gsub(img, "icon16/", "") --remove the icon16 base path
+ img = "icon16/" .. img .. ".png" --why do this? to be able to write any form and let the program fix the form
+ pnl:SetImage(img)
+ end
+ elseif Parts[group] then
+ pnl:SetImage(Parts[group].Icon)
+ else
+ pnl:SetImage("icon16/page_white.png")
+ end
+ if tbl["tooltip"] then
+ pnl:SetTooltip(tbl["tooltip"])
end
+ --trap = false
+ table.sort(tbl, function(a, b) return a < b end)
+ for i, class in pairs(tbl) do
+ if isstring(i) and Parts[class] then
+ local tooltip = pace.TUTORIALS.PartInfos[class].tooltip
- RegisterDermaMenuForClose(sub)
- end)
+ if not tooltip or tooltip == "" then tooltip = "no information available" end
+ if #i > 2 then
+ local part_submenu = add_part(sub, Parts[class])
+ part_submenu:SetTooltip(tooltip)
+ end
+ end
+ end
+ end
end
for i,v in ipairs(other.parts) do
@@ -367,7 +800,7 @@ do -- menu
end
for class_name, part in pairs(partsToShow) do
- local newMenuEntry = menu:AddOption(L((part.FriendlyName or part.ClassName):Replace('_', ' ')), function()
+ local newMenuEntry = menu:AddOption(L((part.FriendlyName or part.ClassName):Replace("_", " ")), function()
pace.RecordUndoHistory()
pace.Call("CreatePart", class_name, nil, nil, parent)
pace.RecordUndoHistory()
@@ -380,9 +813,19 @@ do -- menu
end
function pace.OnAddPartMenu(obj)
+ local event_part_template
+ for _, part in ipairs(pace.GetRegisteredParts()) do
+ if part.ClassName == "event" then
+ event_part_template = part
+ end
+ end
+ local mode = GetConVar("pac_copilot_partsearch_depth"):GetInt()
+ pace.suppress_flashing_property = false
+
local base = vgui.Create("EditablePanel")
base:SetPos(input.GetCursorPos())
base:SetSize(200, 300)
+
base:MakePopup()
function base:OnRemove()
@@ -395,38 +838,212 @@ do -- menu
edit:RequestFocus()
edit:SetUpdateOnType(true)
- local result = base:Add("DPanel")
+ local result = base:Add("DScrollPanel")
result:Dock(FILL)
+ base.search_mode = "classes"
- function edit:OnEnter()
- if result.found[1] then
- pace.RecordUndoHistory()
- pace.Call("CreatePart", result.found[1].ClassName)
- pace.RecordUndoHistory()
+ local function populate_with_sounds(base,result,filter)
+ base.search_mode = "sounds"
+ for _,snd in ipairs(pace.bookmarked_ressources["sound"]) do
+ if filter ~= nil and filter ~= "" then
+ if snd:find(filter, nil, true) then
+ table.insert(result.found, snd)
+ end
+ else
+ table.insert(result.found, snd)
+ end
+ end
+ for _,snd in ipairs(result.found) do
+ if not isstring(snd) then continue end
+ local line = result:Add("DButton")
+ line:SetText("")
+ line:SetTall(20)
+ local btn = line:Add("DImageButton")
+ btn:SetSize(16, 16)
+ btn:SetPos(4,0)
+ btn:CenterVertical()
+ btn:SetMouseInputEnabled(false)
+ local icon = "icon16/sound.png"
+
+ if string.find(snd, "music") or string.find(snd, "theme") then
+ icon = "icon16/music.png"
+ elseif string.find(snd, "loop") then
+ icon = "icon16/arrow_rotate_clockwise.png"
+ end
+
+ btn:SetIcon(icon)
+ local label = line:Add("DLabel")
+ label:SetTextColor(label:GetSkin().Colours.Category.Line.Text)
+ label:SetText(snd)
+ label:SizeToContents()
+ label:MoveRightOf(btn, 4)
+ label:SetMouseInputEnabled(false)
+ label:CenterVertical()
+
+ line.DoClick = function()
+ if pace.current_part.ClassName == "sound" then
+ pace.current_part:SetSound(snd)
+ elseif pace.current_part.ClassName == "sound2" then
+ pace.current_part:SetPath(snd)
+ end
+ pace.PopulateProperties(pace.current_part)
+ base:Remove()
+ end
+
+ line:Dock(TOP)
end
- base:Remove()
end
- edit.OnValueChange = function(_, str)
- result:Clear()
- result.found = {}
+ local function populate_with_models(base,result,filter)
+ base.search_mode = "models"
+ for _,mdl in ipairs(pace.bookmarked_ressources["models"]) do
+ if filter ~= nil and filter ~= "" then
+ if mdl:find(filter, nil, true) then
+ table.insert(result.found, mdl)
+ end
+ else
+ table.insert(result.found, mdl)
+ end
+ end
+ for _,mdl in ipairs(result.found) do
+ if not isstring(mdl) then continue end
+ local line = result:Add("DButton")
+ line:SetText("")
+ line:SetTall(20)
+ local btn = line:Add("DImageButton")
+ btn:SetSize(16, 16)
+ btn:SetPos(4,0)
+ btn:CenterVertical()
+ btn:SetMouseInputEnabled(false)
+ btn:SetIcon("materials/spawnicons/"..string.gsub(mdl, ".mdl", "")..".png")
+ local label = line:Add("DLabel")
+ label:SetTextColor(label:GetSkin().Colours.Category.Line.Text)
+ label:SetText(mdl)
+ label:SizeToContents()
+ label:MoveRightOf(btn, 4)
+ label:SetMouseInputEnabled(false)
+ label:CenterVertical()
- for _, part in ipairs(pace.GetRegisteredParts()) do
- if (part.FriendlyName or part.ClassName):find(str, nil, true) then
- table.insert(result.found, part)
+ line.DoClick = function()
+ if pace.current_part.Model then
+ pace.current_part:SetModel(mdl)
+ pace.PopulateProperties(pace.current_part)
+ end
+ base:Remove()
end
+
+ line:Dock(TOP)
end
+ end
- table.sort(result.found, function(a, b) return #a.ClassName < #b.ClassName end)
+ local function populate_with_events(base,result,filter)
+ base.search_mode = "events"
+ for e,tbl in pairs(event_part_template.Events) do
+ if filter ~= nil and filter ~= "" then
+ if e:find(filter, nil, true) then
+ table.insert(result.found, e)
+ end
+ else
+ table.insert(result.found, e)
+ end
+ end
+ for _,e in ipairs(result.found) do
+ if not isstring(e) then continue end
+ local line = result:Add("DButton")
+ line:SetText("")
+ line:SetTall(20)
+ local btn = line:Add("DImageButton")
+ btn:SetSize(16, 16)
+ btn:SetPos(4,0)
+ btn:CenterVertical()
+ btn:SetMouseInputEnabled(false)
+ btn:SetIcon("icon16/clock.png")
+ local label = line:Add("DLabel")
+ label:SetTextColor(label:GetSkin().Colours.Category.Line.Text)
+ label:SetText(e)
+ label:SizeToContents()
+ label:MoveRightOf(btn, 4)
+ label:SetMouseInputEnabled(false)
+ label:CenterVertical()
+
+ line.DoClick = function()
+ if pace.current_part.Event then
+ pace.current_part:SetEvent(e)
+ pace.PopulateProperties(pace.current_part)
+ end
+ base:Remove()
+ end
+
+ line:Dock(TOP)
+ end
+ end
+ local function populate_with_classes(base, result, filter)
+ for i, part in ipairs(pace.GetRegisteredParts()) do
+ if filter then
+ if (part.FriendlyName or part.ClassName):find(filter, nil, true) then
+ table.insert(result.found, part)
+ end
+ else table.insert(result.found, part) end
+ end
+ table.sort(result.found, function(a, b) return #a.ClassName < #b.ClassName end)
for _, part in ipairs(result.found) do
local line = result:Add("DButton")
line:SetText("")
line:SetTall(20)
+ local remove_now = false
line.DoClick = function()
pace.RecordUndoHistory()
pace.Call("CreatePart", part.ClassName)
- base:Remove()
+
+ if part.ClassName == "event" then
+ remove_now = false
+ result:Clear()
+ result.found = {}
+
+ if mode == 0 then --auto-focus mode
+ remove_now = true
+ timer.Simple(0.1, function()
+ pace.FlashProperty(pace.current_part, "Event", true)
+ end)
+ elseif mode == 1 then --event partsearch
+ pace.suppress_flashing_property = true
+ populate_with_events(base,result,"")
+ edit:SetText("")
+ edit:RequestFocus()
+ else
+ remove_now = true
+ end
+ elseif part.ClassName == "model2" and mode == 1 then --model partsearch
+ remove_now = false
+ result:Clear()
+ result.found = {}
+ populate_with_models(base,result,"")
+ pace.suppress_flashing_property = true
+ edit:SetText("")
+ edit:RequestFocus()
+ elseif (part.ClassName == "sound" or part.ClassName == "sound2") and mode == 1 then
+ remove_now = false
+ result:Clear()
+ result.found = {}
+ populate_with_sounds(base,result,"")
+ pace.suppress_flashing_property = true
+ edit:SetText("")
+ edit:RequestFocus()
+ elseif star_properties[result.found[1].ClassName] and (mode == 0 or GetConVar("pac_copilot_auto_focus_main_property_when_creating_part"):GetBool()) then
+ pace.suppress_flashing_property = false
+ local classname = part.ClassName
+ timer.Simple(0.1, function()
+ pace.FlashProperty(pace.current_part, star_properties[classname], true)
+ end)
+ remove_now = true
+ else
+ remove_now = true
+ end
+ timer.Simple(0.4, function()
+ pace.suppress_flashing_property = false
+ end)
+ if remove_now then base:Remove() end
pace.RecordUndoHistory()
end
@@ -449,7 +1066,7 @@ do -- menu
local label = line:Add("DLabel")
label:SetTextColor(label:GetSkin().Colours.Category.Line.Text)
- label:SetText(L((part.FriendlyName or part.ClassName):Replace('_', ' ')))
+ label:SetText(L((part.FriendlyName or part.ClassName):Replace("_", " ")))
label:SizeToContents()
label:MoveRightOf(btn, 4)
label:SetMouseInputEnabled(false)
@@ -457,8 +1074,109 @@ do -- menu
line:Dock(TOP)
end
+ end
+
+ function edit:OnEnter()
+ local remove_now = true
+ if result.found[1] then
+ if base.search_mode == "classes" then
+ pace.RecordUndoHistory()
+ local part = pace.Call("CreatePart", result.found[1].ClassName)
+ pace.RecordUndoHistory()
+
+ if mode == 1 then
+ if result.found[1].ClassName == "event" then
+ result:Clear()
+ populate_with_events(base,result,"")
+ elseif result.found[1].ClassName == "model2" then
+ result:Clear()
+ populate_with_models(base,result,"")
+ end
+
+ else
+ base:Remove()
+ end
+
+ elseif base.search_mode == "events" then
+ if pace.current_part.Event then
+ pace.current_part:SetEvent()
+ pace.PopulateProperties(pace.current_part)
+ end
+ base:Remove()
+
+ elseif base.search_mode == "models" then
+ if mode == 1 then
+ result:Clear()
+ pace.current_part:SetModel(result.found[1])
+ pace.PopulateProperties(pace.current_part)
+ else
+ base:Remove()
+ end
+ elseif base.search_mode == "sounds" then
+ if mode == 1 then
+ result:Clear()
+ if pace.current_part.ClassName == "sound" then
+ pace.current_part:SetSound(result.found[1])
+ elseif pace.current_part.ClassName == "sound2" then
+ pace.current_part:SetPath(result.found[1])
+ end
+ pace.PopulateProperties(pace.current_part)
+ else
+ base:Remove()
+ end
+
+ end
+
+ if result.found[1].ClassName == "event" then
+ remove_now = false
+ result:Clear()
+ result.found = {}
+ if mode == 0 then
+ remove_now = true
+ timer.Simple(0.1, function()
+ pace.FlashProperty(pace.current_part, "Event", true)
+ end)
+ elseif mode == 1 then
+ pace.suppress_flashing_property = true
+ populate_with_events(base,result,"")
+ edit:SetText("")
+ edit:RequestFocus()
+ else
+ remove_now = true
+ end
+ elseif star_properties[result.found[1].ClassName] and (mode == 0 or GetConVar("pac_copilot_auto_focus_main_property_when_creating_part"):GetBool()) then
+ local classname = result.found[1].ClassName
+ timer.Simple(0.1, function()
+ pace.FlashProperty(pace.current_part, star_properties[classname], true)
+ end)
+ end
+ end
+ timer.Simple(0.4, function()
+ pace.suppress_flashing_property = false
+ end)
+ if remove_now then base:Remove() end
+ end
+
+ edit.OnValueChange = function(_, str)
+ result:Clear()
+ result.found = {}
+ local remove_now = true
+
+ if base.search_mode == "classes" then
+ populate_with_classes(base, result, str)
+
+ elseif base.search_mode == "events" then
+ populate_with_events(base,result,str,event_template)
+
+ elseif base.search_mode == "models" then
+ populate_with_models(base,result,str)
+ elseif base.search_mode == "sounds" then
+ populate_with_sounds(base,result,str)
+ end
+
+ --base:SetHeight(20 * #result.found + edit:GetTall())
+ base:SetHeight(600 + edit:GetTall())
- base:SetHeight(20 * #result.found + edit:GetTall())
end
edit:OnValueChange("")
@@ -470,6 +1188,12 @@ do -- menu
end
end
end)
+
+ timer.Simple(0.1, function()
+ base:MoveToFront()
+ base:RequestFocus()
+ end)
+
end
function pace.Copy(obj)
@@ -511,6 +1235,9 @@ do -- menu
end
function pace.RemovePart(obj)
+ pace.ExtendWearTracker(1)
+ if table.HasValue(pace.BulkSelectList,obj) then table.RemoveByValue(pace.BulkSelectList,obj) end
+
pace.RecordUndoHistory()
obj:Remove()
pace.RecordUndoHistory()
@@ -520,93 +1247,4292 @@ do -- menu
if not obj:HasParent() and obj.ClassName == "group" then
pace.RemovePartOnServer(obj:GetUniqueID(), false, true)
end
+
end
- function pace.OnPartMenu(obj)
- local menu = DermaMenu()
- menu:SetPos(input.GetCursorPos())
+ function pace.SwapBaseMovables(obj1, obj2, promote)
+ if not obj1 or not obj2 then return end
+ if not obj1.Position or not obj1.Angles or not obj2.Position or not obj2.Angles then return end
+ local base_movable_fields = {
+ "Position", "PositionOffset", "Angles", "AngleOffset", "EyeAngles", "AimPart", "AimPartName"
+ }
+ local a_part = obj2
+ local b_part = obj1
- if obj then
- if not obj:HasParent() then
- menu:AddOption(L"wear", function() pace.SendPartToServer(obj) end):SetImage(pace.MiscIcons.wear)
+ if promote then --obj1 takes place of obj2 up or down the hierarchy
+ if obj1.Parent == obj2 then
+ a_part = obj2
+ b_part = obj1
+ elseif obj2.Parent == obj1 then
+ a_part = obj1
+ b_part = obj2
end
+ end
- menu:AddOption(L"copy", function() pace.Copy(obj) end):SetImage(pace.MiscIcons.copy)
- menu:AddOption(L"paste", function() pace.Paste(obj) end):SetImage(pace.MiscIcons.paste)
- menu:AddOption(L"cut", function() pace.Cut(obj) end):SetImage('icon16/cut.png')
- menu:AddOption(L"paste properties", function() pace.PasteProperties(obj) end):SetImage(pace.MiscIcons.replace)
- menu:AddOption(L"clone", function() pace.Clone(obj) end):SetImage(pace.MiscIcons.clone)
+ for i,field in ipairs(base_movable_fields) do
+ local a_val = a_part["Get"..field](a_part)
+ local b_val = b_part["Get"..field](b_part)
+ a_part["Set"..field](a_part, b_val)
+ b_part["Set"..field](b_part, a_val)
+ end
- menu:AddSpacer()
+ if promote then
+ b_part:SetParent(a_part.Parent) b_part:SetEditorExpand(true)
+ a_part:SetParent(b_part) a_part:SetEditorExpand(true)
+ else
+ local a_parent = a_part.Parent
+ local b_parent = b_part.Parent
+
+ a_part:SetParent(b_parent)
+ b_part:SetParent(a_parent)
end
+ pace.RefreshTree()
+ end
- pace.AddRegisteredPartsToMenu(menu, not obj)
+ function pace.SubstituteBaseMovable(obj,action,cast_class)
+ local prompt = (cast_class == nil)
+ cast_class = cast_class or "model2"
+ if action == "create_parent" then
+ local function func(str)
+ if str == "model" then str = "model2" end --I don't care, stop using legacy
+ local newObj = pac.CreatePart(str)
+ if not IsValid(newObj) then return end
- menu:AddSpacer()
+ newObj:SetParent(obj.Parent)
+ obj:SetParent(newObj)
- if obj then
- local save, pnl = menu:AddSubMenu(L"save", function() pace.SaveParts() end)
- pnl:SetImage(pace.MiscIcons.save)
- add_expensive_submenu_load(pnl, function() pace.AddSaveMenuToMenu(save, obj) end)
+ for i,v in pairs(obj:GetChildren()) do
+ v:SetParent(newObj)
+ end
+
+ newObj:SetPosition(obj.Position)
+ newObj:SetPositionOffset(obj.PositionOffset)
+ newObj:SetAngles(obj.Angles)
+ newObj:SetAngleOffset(obj.AngleOffset)
+ newObj:SetEyeAngles(obj.EyeAngles)
+ newObj:SetAimPart(obj.AimPart)
+ newObj:SetAimPartName(obj.AimPartName)
+ newObj:SetBone(obj.Bone)
+ newObj:SetEditorExpand(true)
+
+ obj:SetPosition(Vector(0,0,0))
+ obj:SetPositionOffset(Vector(0,0,0))
+ obj:SetAngles(Angle(0,0,0))
+ obj:SetAngleOffset(Angle(0,0,0))
+ obj:SetEyeAngles(false)
+ obj:SetAimPart(nil)
+ obj:SetAimPartName("")
+ obj:SetBone("head")
+
+ pace.RefreshTree()
+ end
+ if prompt then
+ Derma_StringRequest("Create substitute parent", "Select a class name to create a parent", "model2", function(str) func(str) end)
+ else
+ func(cast_class)
+ end
+ elseif action == "reorder_child" then
+ if obj.Parent then
+ if obj.Parent.Position and obj.Parent.Angles then
+ pace.SwapBaseMovables(obj, obj.Parent, true)
+ end
+ end
+ pace.RefreshTree()
+ elseif action == "cast" then
+ local function func(str)
+ if str == obj.ClassName then return end
+ if str == "model" then str = "model2" end --I don't care, stop using legacy
+ local uid = obj.UniqueID
+
+ if pace.Editor:IsValid() then
+ pace.RefreshTree()
+ pace.Editor:InvalidateLayout()
+ pace.RefreshTree()
+ end
+
+
+ obj.ClassName = str
+
+ timer.Simple(0, function()
+ _G.pac_Restart()
+ if str == "model2" then
+ obj = pac.GetPartFromUniqueID(pac.Hash(pac.LocalPlayer), uid)
+ obj:SetModel("models/pac/default.mdl")
+ end
+
+ end)
+ end
+ if prompt then
+ Derma_StringRequest("Cast", "Select a class name to convert to. Make sure you know what you\'re doing! It will do a pac_restart after!", "model2", function(str) func(str) end)
+ else
+ func(cast_class)
+ end
end
+ pace.recently_substituted_movable_part = obj
+ end
- local load, pnl = menu:AddSubMenu(L"load", function() pace.LoadParts() end)
- add_expensive_submenu_load(pnl, function() pace.AddSavedPartsToMenu(load, false, obj) end)
+ function pace.ClearBulkList(silent)
+ for _,v in ipairs(pace.BulkSelectList) do
+ if IsValid(v.pace_tree_node) then v.pace_tree_node:SetAlpha( 255 ) end
+ v:SetInfo()
+ end
+ pace.BulkSelectList = {}
+ if not silent then pac.Message("Bulk list deleted!") end
+ --surface.PlaySound('buttons/button16.wav')
+ end
+//@note pace.DoBulkSelect
+ function pace.DoBulkSelect(obj, silent)
+ obj = obj or pace.current_part
+ refresh_halo_hook = false
+ --print(obj.pace_tree_node, "color", obj.pace_tree_node:GetFGColor().r .. " " .. obj.pace_tree_node:GetFGColor().g .. " " .. obj.pace_tree_node:GetFGColor().b)
+ if obj.ClassName == "timeline_dummy_bone" then return end
+ local selected_part_added = false --to decide the sound to play afterward
- pnl:SetImage(pace.MiscIcons.load)
+ pace.BulkSelectList = pace.BulkSelectList or {}
+ if (table.HasValue(pace.BulkSelectList, obj)) then
+ pace.RemoveFromBulkSelect(obj)
+ pace.FlashNotification("Bulk select: de-selected " .. tostring(obj))
+ selected_part_added = false
+ elseif (pace.BulkSelectList[obj] == nil) then
+ pace.AddToBulkSelect(obj)
+ pace.FlashNotification("Bulk select: selected " .. tostring(obj))
+ selected_part_added = true
+ if bulk_select_subsume:GetBool() then
+ for _,v in ipairs(obj:GetChildrenList()) do
+ pace.RemoveFromBulkSelect(v)
+ end
+ end
+ end
- if obj then
- menu:AddSpacer()
- menu:AddOption(L"remove", function() pace.RemovePart(obj) end):SetImage(pace.MiscIcons.clear)
+ if bulk_select_subsume:GetBool() then
+ --check parents and children
+ for _,v in ipairs(pace.BulkSelectList) do
+ if table.HasValue(v:GetChildrenList(), obj) then
+ --print("selected part is already child to a bulk-selected part!")
+ pace.RemoveFromBulkSelect(obj)
+ pace.FlashNotification("")
+ selected_part_added = false
+ elseif table.HasValue(obj:GetChildrenList(), v) then
+ --print("selected part is already parent to a bulk-selected part!")
+ pace.RemoveFromBulkSelect(v)
+ pace.FlashNotification("")
+ selected_part_added = false
+ end
+ end
+ end
+
+ RebuildBulkHighlight()
+ if not silent then
+ if selected_part_added then
+ surface.PlaySound("buttons/button1.wav")
+
+ else surface.PlaySound("buttons/button16.wav") end
+ end
+
+ if table.IsEmpty(pace.BulkSelectList) then
+ --remove halo hook
+ pac.RemoveHook("PreDrawHalos", "BulkSelectHighlights")
+ else
+ --start halo hook
+ pac.AddHook("PreDrawHalos", "BulkSelectHighlights", function()
+ local mode = GetConVar("pac_bulk_select_halo_mode"):GetInt()
+ if hover_color:GetString() == "none" then return end
+ if mode == 0 then return
+ elseif mode == 1 then ThinkBulkHighlight()
+ elseif mode == 2 then if input.IsKeyDown(input.GetKeyCode(GetConVar("pac_bulk_select_key"):GetString())) then ThinkBulkHighlight() end
+ elseif mode == 3 then if input.IsControlDown() then ThinkBulkHighlight() end
+ elseif mode == 4 then if input.IsShiftDown() then ThinkBulkHighlight() end
+ end
+ end)
end
- menu:Open()
- menu:MakePopup()
end
- function pace.OnNewPartMenu()
- pace.current_part = NULL
- local menu = DermaMenu()
- menu:MakePopup()
- menu:SetPos(input.GetCursorPos())
+ function pace.RemoveFromBulkSelect(obj)
+ table.RemoveByValue(pace.BulkSelectList, obj)
+ if IsValid(obj.pace_tree_node) then
+ obj.pace_tree_node:SetAlpha( 255 )
+ end
+ obj:SetInfo()
+ --RebuildBulkHighlight()
+ end
- pace.AddRegisteredPartsToMenu(menu)
+ function pace.AddToBulkSelect(obj)
+ table.insert(pace.BulkSelectList, obj)
+ if obj.pace_tree_node == nil then return end
+ obj:SetInfo("selected in bulk select")
+ if IsValid(obj.pace_tree_node) then
+ if bulk_select_subsume:GetBool() then
+ obj.pace_tree_node:SetAlpha( 150 )
+ else
+ obj.pace_tree_node:SetAlpha( 255 )
+ end
+
+ end
+ --RebuildBulkHighlight()
+ end
+ function pace.BulkHide()
+ if #pace.BulkSelectList == 0 then return end
+ local first_bool = pace.BulkSelectList[1]:GetHide()
+ for _,v in ipairs(pace.BulkSelectList) do
+ v:SetHide(not first_bool)
+ end
+ end
+//@note apply properties
+ function pace.BulkApplyProperties(obj, policy)
+ local basepart = obj
+ --[[if not table.HasValue(pace.BulkSelectList,obj) then
+ basepart = pace.BulkSelectList[1]
+ end]]
- menu:AddSpacer()
+ local Panel = vgui.Create( "DFrame" )
+ Panel:SetSize( 500, 600 )
+ Panel:Center()
+ Panel:SetTitle("BULK SELECT PROPERTY EDIT - WARNING! EXPERIMENTAL FEATURE!")
- local load, pnl = menu:AddSubMenu(L"load", function() pace.LoadParts() end)
- pnl:SetImage(pace.MiscIcons.load)
- add_expensive_submenu_load(pnl, function() pace.AddSavedPartsToMenu(load, false, obj) end)
+ Panel:MakePopup()
+ surface.CreateFont("Font", {
+ font = "Arial",
+ extended = true,
+ weight = 700,
+ size = 15
+ })
- menu:AddOption(L"clear", function()
- pace.ClearParts()
- end):SetImage(pace.MiscIcons.clear)
+ local scroll_panel = vgui.Create("DScrollPanel", Panel)
+ scroll_panel:SetSize( 500, 540 )
+ scroll_panel:SetPos( 0, 60 )
+ local thoroughness_tickbox = vgui.Create("DCheckBox", Panel)
+ thoroughness_tickbox:SetSize(20,20)
+ thoroughness_tickbox:SetPos( 5, 30 )
+ local thoroughness_tickbox_label = vgui.Create("DLabel", Panel)
+ thoroughness_tickbox_label:SetSize(150,30)
+ thoroughness_tickbox_label:SetPos( 30, 25 )
+ thoroughness_tickbox_label:SetText("Affect children?")
+ thoroughness_tickbox_label:SetFont("Font")
+ local basepart_label = vgui.Create("DLabel", Panel)
+ basepart_label:SetSize(340,30)
+ basepart_label:SetPos( 160, 25 )
+ local partinfo = basepart.ClassName
+ if basepart.ClassName == "event" then partinfo = basepart.Event .. " " .. partinfo end
+ local partinfo_icon = vgui.Create("DImage",basepart_label)
+ partinfo_icon:SetSize(30,30)
+ partinfo_icon:SetPos( 300, 0 )
+ partinfo_icon:SetImage(basepart.Icon)
+
+ basepart_label:SetText("base part: "..partinfo)
+ basepart_label:SetFont("Font")
+
+ local excluded_vars = {
+ ["Duplicate"] = true,
+ ["OwnerName"] = true,
+ ["ParentUID"] = true,
+ ["UniqueID"] = true,
+ ["TargetEntityUID"] = true
+ }
+
+ local shared_properties = {}
+ local shared_udata_properties = {}
+
+ for _,prop in pairs(basepart:GetProperties()) do
+
+ local shared = true
+ for _,part2 in pairs(pace.BulkSelectList) do
+ if basepart ~= part2 and basepart.ClassName ~= part2.ClassName then
+ if part2["Get" .. prop["key"]] == nil then
+ if policy == "harsh" then shared = false end
+ end
+ end
+ end
+ if shared and not prop.udata.editor_friendly and basepart["Get" .. prop["key"]] ~= nil then
+ shared_properties[#shared_properties + 1] = prop["key"]
+ elseif shared and prop.udata.editor_friendly and basepart["Get" .. prop["key"]] == nil then
+ if not table.HasValue(shared_udata_properties, "event_udata_"..prop["key"]) then
+ shared_udata_properties[#shared_udata_properties + 1] = "event_udata_"..prop["key"]
+ end
+ end
+ end
+
+ if policy == "lenient" then
+ local initial_shared_properties = table.Copy(shared_properties)
+ local initial_shared_udata_properties = table.Copy(shared_udata_properties)
+ for _,part2 in pairs(pace.BulkSelectList) do
+ for _,prop in ipairs(part2:GetProperties()) do
+ if not (table.HasValue(shared_properties, prop["key"]) or table.HasValue(shared_udata_properties, "event_udata_"..prop["key"])) then
+ if part2["Get" .. prop["key"]] ~= nil then
+ initial_shared_properties[#initial_shared_properties + 1] = prop["key"]
+ elseif part2["Get" .. prop["key"]] == nil then
+ if not table.HasValue(initial_shared_udata_properties, "event_udata_"..prop["key"]) then
+ initial_shared_udata_properties[#initial_shared_udata_properties + 1] = "event_udata_"..prop["key"]
+ end
+ end
+ end
+ end
+ end
+ shared_properties = initial_shared_properties
+ shared_udata_properties = initial_shared_udata_properties
+ end
+
+ for i,v in ipairs(shared_properties) do
+ if excluded_vars[v] then table.remove(shared_properties,i) end
+ end
+
+ --populate panels for standard GetSet part properties
+ for i,v in pairs(shared_properties) do
+ local VAR_PANEL = vgui.Create("DFrame")
+ VAR_PANEL:SetSize(500,30)
+ VAR_PANEL:SetPos(0,0)
+ VAR_PANEL:ShowCloseButton( false )
+ local VAR_PANEL_BUTTON = VAR_PANEL:Add("DButton")
+ VAR_PANEL_BUTTON:SetSize(80,30)
+ VAR_PANEL_BUTTON:SetPos(400,0)
+ local VAR_PANEL_EDITZONE
+ local var_type
+ for _,testpart in ipairs(pace.BulkSelectList) do
+ if
+ testpart["Get" .. v] ~= nil
+ then
+ var_type = type(testpart["Get" .. v](testpart))
+ end
+ end
+ if basepart["Get" .. v] ~= nil then var_type = type(basepart["Get" .. v](basepart)) end
+
+ if var_type == "number" then
+ VAR_PANEL_EDITZONE = vgui.Create("DTextEntry", VAR_PANEL)
+ VAR_PANEL_EDITZONE:SetSize(200,30)
+ elseif var_type == "boolean" then
+ VAR_PANEL_EDITZONE = vgui.Create("DCheckBox", VAR_PANEL)
+ VAR_PANEL_EDITZONE:SetSize(30,30)
+ elseif var_type == "string" then
+ VAR_PANEL_EDITZONE = vgui.Create("DTextEntry", VAR_PANEL)
+ VAR_PANEL_EDITZONE:SetSize(200,30)
+ elseif var_type == "Vector" then
+ VAR_PANEL_EDITZONE = vgui.Create("DTextEntry", VAR_PANEL)
+ VAR_PANEL_EDITZONE:SetSize(200,30)
+ elseif var_type == "Angle" then
+ VAR_PANEL_EDITZONE = vgui.Create("DTextEntry", VAR_PANEL)
+ VAR_PANEL_EDITZONE:SetSize(200,30)
+ else
+ VAR_PANEL_EDITZONE = vgui.Create("DTextEntry", VAR_PANEL)
+ VAR_PANEL_EDITZONE:SetSize(200,30)
+ end
+ VAR_PANEL_EDITZONE:SetPos(200,0)
+
+ VAR_PANEL_BUTTON:SetText("APPLY")
+
+ VAR_PANEL:SetTitle("[" .. i .. "] "..v.." "..var_type)
+
+ VAR_PANEL:Dock( TOP )
+ VAR_PANEL:DockMargin( 5, 0, 0, 5 )
+ VAR_PANEL_BUTTON.DoClick = function()
+ for i,part in pairs(pace.BulkSelectList) do
+ local sent_var
+ if var_type == "number" then
+ sent_var = VAR_PANEL_EDITZONE:GetValue()
+ if not tonumber(sent_var) then
+ local ok, res = pac.CompileExpression(sent_var)
+ if ok then
+ sent_var = res() or 0
+ end
+ end
+ elseif var_type == "boolean" then
+ sent_var = VAR_PANEL_EDITZONE:GetChecked()
+ elseif var_type == "string" then
+ sent_var = VAR_PANEL_EDITZONE:GetValue()
+ if v == "Name" and sent_var ~= "" then
+ sent_var = sent_var..i
+ end
+ elseif var_type == "Vector" then
+ local str = string.Split(VAR_PANEL_EDITZONE:GetValue(), ",")
+ sent_var = Vector()
+ sent_var.x = tonumber(str[1]) or 1
+ sent_var.y = tonumber(str[2]) or 1
+ sent_var.z = tonumber(str[3]) or 1
+ if v == "Color" and not part.ProperColorRange then sent_var = sent_var*255 end
+ elseif var_type == "Angle" then
+ local str = string.Split(VAR_PANEL_EDITZONE:GetValue(), ",")
+ sent_var = Angle()
+ sent_var.p = tonumber(str[1]) or 1
+ sent_var.y = tonumber(str[2]) or 1
+ sent_var.r = tonumber(str[3]) or 1
+ else sent_var = VAR_PANEL_EDITZONE:GetValue() end
+
+
+ if policy == "harsh" then part["Set" .. v](part, sent_var)
+ elseif policy == "lenient" then
+ if part["Get" .. v] ~= nil then part["Set" .. v](part, sent_var) end
+ end
+ if thoroughness_tickbox:GetChecked() then
+ for _,child in pairs(part:GetChildrenList()) do
+ if part["Get" .. v] ~= nil then child["Set" .. v](child, sent_var) end
+ end
+ end
+ end
+
+ pace.RefreshTree(true)
+ timer.Simple(0.3, function() BulkSelectRefreshFadedNodes() end)
+ end
+ scroll_panel:AddItem( VAR_PANEL )
+ end
+
+ --populate panels for event "userdata" packaged into arguments
+ if #shared_udata_properties > 0 then
+ local fallback_event_types = {}
+ local fallback_event
+ for i,v in ipairs(pace.BulkSelectList) do
+ if v.ClassName == "event" then
+ table.Add(fallback_event_types,v.Event)
+ fallback_event = v
+ end
+ end
+
+ --[[example udata arg from part.Events[part.Event].__registeredArguments
+ 1:
+ 1 = button
+ 2 = string
+ 3:
+ default = mouse_left
+ enums = function: 0xa88929ea
+ group = arguments
+ ]]
+
+ local function GetEventArgType(part, str)
+ if not part.Events then return "string" end
+ for argn,arg in ipairs(part.Events[part.Event].__registeredArguments) do
+ if arg[1] == str then
+ return arg[2]
+ end
+ end
+ if fallback_event then
+ for i,e in ipairs(fallback_event_types) do
+ for argn,arg in ipairs(fallback_event.Events[e].__registeredArguments) do
+ if arg[1] == str then
+ return arg[2]
+ end
+ end
+ end
+ end
+ return "string"
+ end
+
+ local function GetEventArgIndex(part,str)
+ str = string.gsub(str, "event_udata_", "")
+
+ for argn,arg in ipairs(part.Events[part.Event].__registeredArguments) do
+ if arg[1] == str then
+ return argn
+ end
+ end
+ return 1
+ end
+
+ local function ApplyArgToIndex(args_str, str, index)
+ local args_tbl = string.Split(args_str,"@@")
+ args_tbl[index] = str
+ return table.concat(args_tbl,"@@")
+ end
+
+ for i,v in ipairs(shared_udata_properties) do
+
+ local udata_val_name = string.gsub(v, "event_udata_", "")
+
+ local var_type = GetEventArgType(obj, udata_val_name)
+ if var_type == nil then var_type = "string" end
+
+ local VAR_PANEL = vgui.Create("DFrame")
+
+ VAR_PANEL:SetSize(500,30)
+ VAR_PANEL:SetPos(0,0)
+ VAR_PANEL:ShowCloseButton( false )
+ local VAR_PANEL_BUTTON = VAR_PANEL:Add("DButton")
+ VAR_PANEL_BUTTON:SetSize(80,30)
+ VAR_PANEL_BUTTON:SetPos(400,0)
+ local VAR_PANEL_EDITZONE
+ if var_type == "number" then
+ VAR_PANEL_EDITZONE = vgui.Create("DTextEntry", VAR_PANEL)
+ VAR_PANEL_EDITZONE:SetSize(200,30)
+ elseif var_type == "boolean" then
+ VAR_PANEL_EDITZONE = vgui.Create("DCheckBox", VAR_PANEL)
+ VAR_PANEL_EDITZONE:SetSize(30,30)
+ elseif var_type == "string" then
+ VAR_PANEL_EDITZONE = vgui.Create("DTextEntry", VAR_PANEL)
+ VAR_PANEL_EDITZONE:SetSize(200,30)
+ elseif var_type == "Vector" then
+ VAR_PANEL_EDITZONE = vgui.Create("DTextEntry", VAR_PANEL)
+ VAR_PANEL_EDITZONE:SetSize(200,30)
+ elseif var_type == "Angle" then
+ VAR_PANEL_EDITZONE = vgui.Create("DTextEntry", VAR_PANEL)
+ VAR_PANEL_EDITZONE:SetSize(200,30)
+ else
+ VAR_PANEL_EDITZONE = vgui.Create("DTextEntry", VAR_PANEL)
+ VAR_PANEL_EDITZONE:SetSize(200,30)
+ end
+
+ VAR_PANEL_EDITZONE:SetPos(200,0)
+ VAR_PANEL:SetTitle("[" .. i .. "] "..udata_val_name.." "..var_type)
+ VAR_PANEL_BUTTON:SetText("APPLY")
+
+
+ VAR_PANEL:Dock( TOP )
+ VAR_PANEL:DockMargin( 5, 0, 0, 5 )
+ VAR_PANEL_BUTTON.DoClick = function()
+
+ for i,part in ipairs(pace.BulkSelectList) do
+ --PrintTable(part.Events[part.Event].__registeredArguments)
+ local sent_var
+ if var_type == "number" then
+ sent_var = VAR_PANEL_EDITZONE:GetValue()
+ if not tonumber(sent_var) then
+ local ok, res = pac.CompileExpression(sent_var)
+ if ok then
+ sent_var = res() or 0
+ end
+ end
+ elseif var_type == "boolean" then
+ sent_var = VAR_PANEL_EDITZONE:GetChecked()
+ if sent_var == true then sent_var = "1"
+ else sent_var = "0" end
+ elseif var_type == "string" then
+ sent_var = VAR_PANEL_EDITZONE:GetValue()
+ if v == "Name" and sent_var ~= "" then
+ sent_var = sent_var..i
+ end
+ else sent_var = VAR_PANEL_EDITZONE:GetValue() end
+ if part.ClassName == "event" and part.Event == basepart.Event then
+ part:SetArguments(ApplyArgToIndex(part:GetArguments(), sent_var, GetEventArgIndex(part,v)))
+ else
+ part:SetProperty(udata_val_name, sent_var)
+ end
+ if thoroughness_tickbox:GetChecked() then
+ for _,child in pairs(part:GetChildrenList()) do
+ if child.ClassName == "event" and child.Event == basepart.Event then
+ local sent_var
+ if var_type == "number" then
+ sent_var = VAR_PANEL_EDITZONE:GetValue()
+ if not tonumber(sent_var) then
+ local ok, res = pac.CompileExpression(sent_var)
+ if ok then
+ sent_var = res() or 0
+ end
+ end
+ elseif var_type == "boolean" then
+ sent_var = VAR_PANEL_EDITZONE:GetChecked()
+ if sent_var == true then sent_var = "1"
+ else sent_var = "0" end
+ elseif var_type == "string" then
+ sent_var = VAR_PANEL_EDITZONE:GetValue()
+ if v == "Name" and sent_var ~= "" then
+ sent_var = sent_var..i
+ end
+ else sent_var = VAR_PANEL_EDITZONE:GetValue() end
+
+ child:SetArguments(ApplyArgToIndex(child:GetArguments(), sent_var, GetEventArgIndex(child,v)))
+ end
+ end
+ end
+ end
+
+ pace.RefreshTree(true)
+ timer.Simple(0.3, function() BulkSelectRefreshFadedNodes() end)
+ end
+ scroll_panel:AddItem( VAR_PANEL )
+ end
+ end
end
-end
-do
- pac.haloex = include("pac3/libraries/haloex.lua")
+ function pace.BulkCutPaste(obj)
+ pace.RecordUndoHistory()
+ for _,v in ipairs(pace.BulkSelectList) do
+ --if a part is inserted onto itself, it should instead serve as a parent
+ if v ~= obj then v:SetParent(obj) end
+ end
+ pace.RecordUndoHistory()
+ pace.RefreshTree()
+ end
- function pace.OnHoverPart(self)
- local tbl = {}
- local ent = self:GetOwner()
+ function pace.BulkCutPasteOrdered() --two-state operation
+ --first to define an ordered list of parts to move, from bulk select
+ --second to transfer these parts to bulk select list
+ if not pace.ordered_operation_readystate then
+ pace.temp_bulkselect_orderedlist = {}
+ for i,v in ipairs(pace.BulkSelectList) do
+ pace.temp_bulkselect_orderedlist[i] = v
+ end
+ pace.ordered_operation_readystate = true
+ pace.ClearBulkList()
+ pace.FlashNotification("Selected " .. #pace.temp_bulkselect_orderedlist .. " parts for Ordered Insert. Now select " .. #pace.temp_bulkselect_orderedlist .. " parts destinations.")
+ surface.PlaySound("buttons/button4.wav")
+ else
+ if #pace.temp_bulkselect_orderedlist == #pace.BulkSelectList then
+ pace.RecordUndoHistory()
+ for i,v in ipairs(pace.BulkSelectList) do
+ pace.temp_bulkselect_orderedlist[i]:SetParent(v)
+ end
+ pace.RecordUndoHistory()
+ pace.RefreshTree()
+ surface.PlaySound("buttons/button6.wav")
+ end
+ pace.ordered_operation_readystate = false
+ end
+ end
+
+ function pace.BulkCopy(obj)
+ if #pace.BulkSelectList == 1 then pace.Copy(obj) end --at least if there's one selected, we can take it that we want to copy that part
+ pace.BulkSelectClipboard = table.Copy(pace.BulkSelectList) --if multiple parts are selected, copy it to a new bulk clipboard
+ print("[PAC3 bulk select] copied: ")
+ TestPrintTable(pace.BulkSelectClipboard,"pace.BulkSelectClipboard")
+ end
+
+ function pace.BulkPasteFromBulkClipboard(obj) --paste bulk clipboard into one part
+ pace.RecordUndoHistory()
+ if not table.IsEmpty(pace.BulkSelectClipboard) then
+ for _,v in ipairs(pace.BulkSelectClipboard) do
+ local newObj = pac.CreatePart(v.ClassName)
+ newObj:SetTable(v:ToTable(), true)
+ newObj:SetParent(obj)
+ end
+ end
+ pace.RecordUndoHistory()
+ --timer.Simple(0.3, function BulkSelectRefreshFadedNodes(obj) end)
+ end
- if ent:IsValid() then
- table.insert(tbl, ent)
+ function pace.BulkPasteFromBulkSelectToSinglePart(obj) --paste bulk selection into one part
+ pace.RecordUndoHistory()
+ if not table.IsEmpty(pace.BulkSelectList) then
+ for _,v in ipairs(pace.BulkSelectList) do
+ local newObj = pac.CreatePart(v.ClassName)
+ newObj:SetTable(v:ToTable(), true)
+ newObj:SetParent(obj)
+ end
end
+ pace.RecordUndoHistory()
+ end
- for _, child in ipairs(self:GetChildrenList()) do
- local ent = self:GetOwner()
- if ent:IsValid() then
- table.insert(tbl, ent)
+ function pace.BulkPasteFromSingleClipboard() --paste the normal clipboard into each bulk select item
+ pace.RecordUndoHistory()
+ if not table.IsEmpty(pace.BulkSelectList) then
+ for _,v in ipairs(pace.BulkSelectList) do
+ local newObj = pac.CreatePart(pace.Clipboard.self.ClassName)
+ newObj:SetTable(pace.Clipboard, true)
+ newObj:SetParent(v)
end
end
+ pace.RecordUndoHistory()
+ --timer.Simple(0.3, function BulkSelectRefreshFadedNodes(obj) end)
+ end
- if #tbl > 0 then
- local pulse = math.sin(pac.RealTime * 20) * 0.5 + 0.5
- pulse = pulse * 255
- pac.haloex.Add(tbl, Color(pulse, pulse, pulse, 255), 1, 1, 1, true, true, 5, 1, 1)
+ function pace.BulkPasteFromBulkClipboardToBulkSelect()
+ for _,v in ipairs(pace.BulkSelectList) do
+ pace.BulkPasteFromBulkClipboard(v)
end
end
-end
+
+ function pace.BulkRemovePart()
+ pace.RecordUndoHistory()
+ if not table.IsEmpty(pace.BulkSelectList) then
+ for _,v in ipairs(pace.BulkSelectList) do
+ v:Remove()
+
+ if not v:HasParent() and v.ClassName == "group" then
+ pace.RemovePartOnServer(v:GetUniqueID(), false, true)
+ end
+ end
+ end
+ pace.RefreshTree()
+ pace.RecordUndoHistory()
+ pace.ClearBulkList()
+ --timer.Simple(0.1, function BulkSelectRefreshFadedNodes() end)
+ end
+
+ function pace.BulkMorphProperty()
+ if #pace.BulkSelectList == 0 then timer.Simple(0.3, function()
+ pace.FlashNotification("Bulk Morph Property needs parts in bulk select!") end)
+ end
+
+ local parts_backup_properties_values = {}
+ local excluded_properties = {["ParentUID"] = true,["UniqueID"] = true}
+ for i,v in ipairs(pace.BulkSelectList) do
+ parts_backup_properties_values[v] = {}
+ for _,prop in pairs(v:GetProperties()) do
+ if not excluded_properties[prop.key] then
+ parts_backup_properties_values[v][prop.key] = v["Get"..prop.key](v)
+ end
+ end
+ end
+
+ local main_panel = vgui.Create("DFrame")
+ main_panel:SetTitle("Morph properties")
+ main_panel:SetSize(400,280)
+
+ local properties_pnl = pace.CreatePanel("properties", main_panel) properties_pnl:SetSize(380,150) properties_pnl:SetPos(10,125)
+ local start_value = pace.CreatePanel("properties_number") properties_pnl:AddKeyValue("StartValue",start_value)
+ local end_value = pace.CreatePanel("properties_number") properties_pnl:AddKeyValue("EndValue",end_value)
+ start_value:SetNumberValue(1) end_value:SetNumberValue(1)
+ local function swap_properties(property, property_type, success)
+ properties_pnl:Clear()
+ properties_pnl:InvalidateLayout()
+ --timer.Simple(0.2, function()
+ if success then
+ start_value = pace.CreatePanel("properties_" .. property_type, properties_pnl) properties_pnl:AddKeyValue("StartValue",start_value)
+ end_value = pace.CreatePanel("properties_" .. property_type, properties_pnl) properties_pnl:AddKeyValue("EndValue",end_value)
+ if property_type == "vector" then
+ function start_value.OnValueChanged(val)
+ if isstring(val) then
+ if val == "" then return end
+ local x,y,z = unpack(string.Split(val, " "))
+ val = Vector(x,y,z)
+ end
+ start_value:SetValue(val)
+ end
+ function end_value.OnValueChanged(val)
+ if isstring(val) then
+ if val == "" then return end
+ local x,y,z = unpack(string.Split(val, " "))
+ val = Vector(x,y,z)
+ end
+ end_value:SetValue(val)
+ end
+ elseif property_type == "angle" then
+ function start_value.OnValueChanged(val)
+ if isstring(val) then
+ if val == "" then return end
+ local x,y,z = unpack(string.Split(val, " "))
+ val = Angle(x,y,z)
+ end
+ start_value:SetValue(val)
+ end
+ function end_value.OnValueChanged(val)
+ if isstring(val) then
+ if val == "" then return end
+ local x,y,z = unpack(string.Split(val, " "))
+ val = Angle(x,y,z)
+ end
+ end_value:SetValue(val)
+ end
+ elseif property_type == "color" then
+ function start_value.OnValueChanged(val)
+ if isstring(val) then
+ if val == "" then return end
+ local x,y,z = unpack(string.Split(val, " "))
+ val = Color(x,y,z)
+ end
+ start_value:SetValue(val)
+ end
+ function end_value.OnValueChanged(val)
+ if isstring(val) then
+ if val == "" then return end
+ local x,y,z = unpack(string.Split(val, " "))
+ val = Color(x,y,z)
+ end
+ end_value:SetValue(val)
+ end
+ elseif property_type == "number" then
+ function start_value.OnValueChanged(val)
+ start_value:SetValue(tonumber(val) or val)
+ end
+ function end_value.OnValueChanged(val)
+ end_value:SetValue(tonumber(val) or val)
+ end
+ end
+ else
+ start_value = pace.CreatePanel("properties_label", properties_pnl) properties_pnl:AddKeyValue("ERROR",start_value)
+ end_value = pace.CreatePanel("properties_label", properties_pnl) properties_pnl:AddKeyValue("ERROR",end_value)
+ end
+ --end)
+ if start_value.Restart then start_value:Restart() end if end_value.Restart then end_value:Restart() end
+ if start_value.OnValueChanged then
+ local def = start_value:GetValue()
+ if pace.BulkSelectList[1] then def = pace.BulkSelectList[1][property] end
+ start_value.OnValueChanged(def)
+ start_value.OnValueChanged(start_value:GetValue())
+ end
+ if end_value.OnValueChanged then
+ local def = end_value:GetValue()
+ if pace.BulkSelectList[1] then def = pace.BulkSelectList[1][property] end
+ end_value.OnValueChanged(def)
+ end_value.OnValueChanged(end_value:GetValue())
+ end
+ end
+
+ local function setsingle(part, property_name, property_type, frac)
+
+ if property_type == "vector" then
+ local start_val = Vector(start_value.left:GetValue(), start_value.middle:GetValue(), start_value.right:GetValue())
+ local end_val = Vector(end_value.left:GetValue(), end_value.middle:GetValue(), end_value.right:GetValue())
+ local delta = end_val - start_val
+ part["Set"..property_name](part, start_val + frac*delta)
+ elseif property_type == "angle" then
+ local start_val = Angle(start_value.left:GetValue(), start_value.middle:GetValue(), start_value.right:GetValue())
+ local end_val = Angle(end_value.left:GetValue(), end_value.middle:GetValue(), end_value.right:GetValue())
+ local delta = end_val - start_val
+ part["Set"..property_name](part, start_val + frac*delta)
+ elseif property_type == "color" then
+ local r1 = start_value.left:GetValue()
+ local g1 = start_value.middle:GetValue()
+ local b1 = start_value.right:GetValue()
+ local r2 = start_value.left:GetValue()
+ local g2 = start_value.middle:GetValue()
+ local b2 = start_value.right:GetValue()
+
+ part["Set"..property_name](part, Color(r1 + frac*(r2-r1), g1 + frac*(g2-g1), b1 + frac*(b2-b1)))
+ elseif property_type == "number" then
+ local start_val = start_value:GetValue()
+ local end_val = end_value:GetValue()
+ local delta = end_val - start_val
+ part["Set"..property_name](part, start_val + frac*delta)
+ end
+ end
+ local function setmultiple(property_name, property_type)
+ if #pace.BulkSelectList <= 1 then return end
+ for i,v in ipairs(pace.BulkSelectList) do
+ local frac = (i-1) / (#pace.BulkSelectList-1)
+ setsingle(v, property_name, property_type, frac)
+ end
+ end
+ local function reset_initial_properties()
+ --self.left = left
+ --self.middle = middle
+ --self.right = right
+ if start_value.left then
+ print(start_value.left:GetValue(), start_value.middle:GetValue(), start_value.right:GetValue())
+ print(end_value.left:GetValue(), end_value.middle:GetValue(), end_value.right:GetValue())
+ else
+ print(start_value:GetValue())
+ print(end_value:GetValue())
+ end
+
+ for part, tbl in pairs(parts_backup_properties_values) do
+ for prop, value in pairs(tbl) do
+ part["Set"..prop](part, value)
+ end
+ end
+ end
+
+ local properties_2 = pace.CreatePanel("properties", main_panel) properties_2:SetSize(380,85) properties_2:SetPos(10,30)
+ local variable_name = ""
+ local full_success = false
+ local found_type = "number"
+ local variable_name_pnl = pace.CreatePanel("properties_string", main_panel) properties_2:AddKeyValue("VariableName", variable_name_pnl)
+ function variable_name_pnl:SetValue(var)
+ local str = tostring(var)
+ variable_name = str
+ self:SetTextColor(self.alt_line and self:GetSkin().Colours.Category.AltLine.Text or self:GetSkin().Colours.Category.Line.Text)
+ self:SetFont(pace.CurrentFont)
+ self:SetText(" " .. str) -- ugh
+ self:SizeToContents()
+
+ if #str > 10 then
+ self:SetTooltip(str)
+ else
+ self:SetTooltip()
+ end
+ self.original_str = str
+ self.original_var = var
+ if self.OnValueSet then
+ self:OnValueSet(str)
+ end
+
+ full_success = true
+ found_type = "number"
+ for _,v in ipairs(pace.BulkSelectList) do
+ if not v["Get"..str] or not v["Set"..str] then full_success = false
+ else
+ if full_success then found_type = string.lower(type(v["Get"..str](v))) end
+ end
+ end
+ swap_properties(str, found_type, full_success)
+ if full_success then setmultiple(str, found_type) end
+ end
+ function variable_name_pnl:EditText()
+ local oldText = self:GetText()
+ self:SetText("")
+
+ local pnl = vgui.Create("DTextEntry")
+ self.editing = pnl
+ pnl:SetFont(pace.CurrentFont)
+ pnl:SetDrawBackground(false)
+ pnl:SetDrawBorder(false)
+ pnl:SetText(self:EncodeEdit(self.original_str or ""))
+ pnl:SetKeyboardInputEnabled(true)
+ pnl:SetDrawLanguageID(false)
+ pnl:RequestFocus()
+ pnl:SelectAllOnFocus(true)
+
+ pnl.OnTextChanged = function() oldText = pnl:GetText() end
+
+ local hookID = tostring({})
+ local textEntry = pnl
+ local delay = os.clock() + 0.1
+
+ pac.AddHook('Think', hookID, function(code)
+ if not IsValid(self) or not IsValid(textEntry) then return pac.RemoveHook('Think', hookID) end
+ if textEntry:IsHovered() or self:IsHovered() then return end
+ if delay > os.clock() then return end
+ if not input.IsMouseDown(MOUSE_LEFT) and not input.IsKeyDown(KEY_ESCAPE) then return end
+ pac.RemoveHook('Think', hookID)
+ self.editing = false
+ pace.BusyWithProperties = NULL
+ textEntry:Remove()
+ self:SetText(oldText)
+ pnl:OnEnter()
+ end)
+
+ --local x,y = pnl:GetPos()
+ --pnl:SetPos(x+3,y-4)
+ --pnl:Dock(FILL)
+ local x, y = self:LocalToScreen()
+ local inset_x = self:GetTextInset()
+ pnl:SetPos(x+5 + inset_x, y)
+ pnl:SetSize(self:GetSize())
+ pnl:SetWide(ScrW())
+ pnl:MakePopup()
+
+ pnl.OnEnter = function()
+ pace.BusyWithProperties = NULL
+ self.editing = false
+
+ pnl:Remove()
+ self:SetText(pnl:GetText())
+ self:SetValue(pnl:GetText())
+ end
+
+ local old = pnl.Paint
+ pnl.Paint = function(...)
+ if not self:IsValid() then pnl:Remove() return end
+
+ surface.SetFont(pnl:GetFont())
+ local w = surface.GetTextSize(pnl:GetText()) + 6
+
+ surface.DrawRect(0, 0, w, pnl:GetTall())
+ surface.SetDrawColor(self:GetSkin().Colours.Properties.Border)
+ surface.DrawOutlinedRect(0, 0, w, pnl:GetTall())
+
+ pnl:SetWide(w)
+
+ old(...)
+ end
+
+ pace.BusyWithProperties = pnl
+ end
+ variable_name_pnl:SetValue(variable_name)
+ local btn = vgui.Create("DButton", variable_name_pnl)
+ btn:SetSize(16, 16)
+ btn:Dock(RIGHT)
+ btn:SetText("...")
+ btn.DoClick = function()
+ do
+ local get_list = function()
+ local enums = {}
+ local excluded_properties = {["ParentUID"] = true,["UniqueID"] = true}
+ for i,v in ipairs(pace.BulkSelectList) do
+ for _,prop in pairs(v:GetProperties()) do
+ if not excluded_properties[prop.key] and type(v["Get"..prop.key](v)) ~= "string" and type(v["Get"..prop.key](v)) ~= "boolean" then
+ enums[prop.key] = prop.key
+ end
+ end
+ end
+ return enums
+ end
+ pace.SafeRemoveSpecialPanel()
+
+ local frame = vgui.Create("DFrame")
+ frame:SetTitle("Variable name")
+ frame:SetSize(300, 300)
+ frame:Center()
+ frame:SetSizable(true)
+
+ local list = vgui.Create("DListView", frame)
+ list:Dock(FILL)
+ list:SetMultiSelect(false)
+ list:AddColumn("Variable name", 1)
+
+ list.OnRowSelected = function(_, id, line)
+ local val = line.list_key
+ variable_name_pnl:SetValue(val)
+ variable_name = val
+ end
+
+ local first = NULL
+
+ local function build(find)
+ list:Clear()
+
+ for key, val in pairs(get_list()) do
+ local pnl = list:AddLine(key) pnl.list_key = key
+ end
+ end
+
+ local search = vgui.Create("DTextEntry", frame)
+ search:Dock(BOTTOM)
+ search.OnTextChanged = function() build(search:GetValue()) end
+ search.OnEnter = function() if first:IsValid() then list:SelectItem(first) end frame:Remove() end
+ search:RequestFocus()
+ frame:MakePopup()
+
+ build()
+
+ pace.ActiveSpecialPanel = frame
+ end
+ end
+
+ --
+
+ local reset_button = vgui.Create("DButton", main_panel)
+ reset_button:SetText("reset") reset_button:SetSize(190,30)
+ properties_2:AddKeyValue("revert", reset_button)
+ function reset_button:DoClick() reset_initial_properties() end
+
+ local apply_button = vgui.Create("DButton", main_panel)
+ apply_button:SetText("confirm") apply_button:SetSize(190,30)
+ properties_2:AddKeyValue("apply", apply_button)
+ function apply_button:DoClick() if full_success then setmultiple(variable_name, found_type) end end
+ main_panel:Center()
+ end
+
+ function pace.CopyUID(obj)
+ pace.Clipboard = obj.UniqueID
+ SetClipboardText("\"" .. obj.UniqueID .. "\"")
+ pace.FlashNotification(tostring(obj) .. " UID " .. obj.UniqueID .. " has been copied")
+ end
+//@note part menu
+ function pace.OnPartMenu(obj)
+ local menu = DermaMenu()
+ menu:SetPos(input.GetCursorPos())
+ --new_operations_order
+ --default_operations_order
+ --if not obj then obj = pace.current_part end
+ if obj then pace.AddClassSpecificPartMenuComponents(menu, obj) end
+ local contained_bulk_select = false
+ for _,option_name in ipairs(pace.operations_order) do
+ if option_name == "bulk_select" then
+ contained_bulk_select = true
+ end
+ pace.addPartMenuComponent(menu, obj, option_name)
+ end
+
+ if #pace.BulkSelectList >= 1 and not contained_bulk_select then
+ menu:AddSpacer()
+ pace.addPartMenuComponent(menu, obj, "bulk_select")
+ end
+
+ --[[if obj then
+ if not obj:HasParent() then
+ menu:AddOption(L"wear", function()
+ pace.SendPartToServer(obj)
+ pace.BulkSelectList = {}
+ end):SetImage(pace.MiscIcons.wear)
+ end
+
+ menu:AddOption(L"copy", function() pace.Copy(obj) end):SetImage(pace.MiscIcons.copy)
+ menu:AddOption(L"paste", function() pace.Paste(obj) end):SetImage(pace.MiscIcons.paste)
+ menu:AddOption(L"cut", function() pace.Cut(obj) end):SetImage('icon16/cut.png')
+ menu:AddOption(L"paste properties", function() pace.PasteProperties(obj) end):SetImage(pace.MiscIcons.replace)
+ menu:AddOption(L"clone", function() pace.Clone(obj) end):SetImage(pace.MiscIcons.clone)
+
+ local part_size_info, psi_icon = menu:AddSubMenu(L"get part size information", function()
+ local function GetTableSizeInfo(obj_arg)
+ if not IsValid(obj_arg) then return {
+ raw_bytes = 0,
+ info = ""
+ } end
+ local charsize = #util.TableToJSON(obj_arg:ToTable())
+
+ local kilo_range = -1
+ local remainder = charsize*2
+ while remainder / 1000 > 1 do
+ kilo_range = kilo_range + 1
+ remainder = remainder / 1000
+ end
+ local unit = ""
+ if kilo_range == -1 then
+ unit = "B"
+ elseif kilo_range == 0 then
+ unit = "KB"
+ elseif (kilo_range == 1) then
+ unit = "MB"
+ elseif (kilo_range == 2) then
+ unit = "GB"
+ end
+ return {
+ raw_bytes = charsize*2,
+ info = "raw JSON table size: " .. charsize*2 .. " bytes (" .. remainder .. " " .. unit .. ")"
+ }
+ end
+
+ local part_size_info = GetTableSizeInfo(obj)
+ local part_size_info_root = GetTableSizeInfo(obj:GetRootPart())
+
+ local part_size_info_root_processed = "\t" .. math.Round(100 * part_size_info.raw_bytes / part_size_info_root.raw_bytes,1) .. "% share of root "
+
+ local part_size_info_parent
+ local part_size_info_parent_processed
+ if IsValid(obj.Parent) then
+ part_size_info_parent = GetTableSizeInfo(obj.Parent)
+ part_size_info_parent_processed = "\t" .. math.Round(100 * part_size_info.raw_bytes / part_size_info_parent.raw_bytes,1) .. "% share of parent "
+ pac.Message(
+ obj, " " ..
+ part_size_info.info.."\n"..
+ part_size_info_parent_processed,obj.Parent,"\n"..
+ part_size_info_root_processed,obj:GetRootPart()
+ )
+ else
+ pac.Message(
+ obj, " " ..
+ part_size_info.info.."\n"..
+ part_size_info_root_processed,obj:GetRootPart()
+ )
+ end
+
+ end)
+ psi_icon:SetImage('icon16/drive.png')
+
+ part_size_info:AddOption(L"from bulk select", function()
+ local cumulative_bytes = 0
+ for _,v in pairs(pace.BulkSelectList) do
+ cumulative_bytes = cumulative_bytes + 2*#util.TableToJSON(v:ToTable())
+ end
+ local kilo_range = -1
+ local remainder = cumulative_bytes
+ while remainder / 1000 > 1 do
+ kilo_range = kilo_range + 1
+ remainder = remainder / 1000
+ end
+ local unit = ""
+ if kilo_range == -1 then
+ unit = "B"
+ elseif kilo_range == 0 then
+ unit = "KB"
+ elseif (kilo_range == 1) then
+ unit = "MB"
+ elseif (kilo_range == 2) then
+ unit = "GB"
+ end
+ pac.Message("Bulk selected parts total " .. remainder .. unit)
+ end
+ )
+
+ local bulk_apply_properties,bap_icon = menu:AddSubMenu(L"bulk change properties", function() pace.BulkApplyProperties(obj, "harsh") end)
+ bap_icon:SetImage('icon16/table_multiple.png')
+ bulk_apply_properties:AddOption("Policy: harsh filtering", function() pace.BulkApplyProperties(obj, "harsh") end)
+ bulk_apply_properties:AddOption("Policy: lenient filtering", function() pace.BulkApplyProperties(obj, "lenient") end)
+
+ --bulk select
+ bulk_menu, bs_icon = menu:AddSubMenu(L"bulk select ("..#pace.BulkSelectList..")", function() pace.DoBulkSelect(obj) end)
+ bs_icon:SetImage('icon16/table_multiple.png')
+ bulk_menu.GetDeleteSelf = function() return false end
+
+ local mode = GetConVar("pac_bulk_select_halo_mode"):GetInt()
+ local info
+ if mode == 0 then info = "not halo-highlighted"
+ elseif mode == 1 then info = "automatically halo-highlighted"
+ elseif mode == 2 then info = "halo-highlighted on custom keypress:"..GetConVar("pac_bulk_select_halo_key"):GetString()
+ elseif mode == 3 then info = "halo-highlighted on preset keypress: control"
+ elseif mode == 4 then info = "halo-highlighted on preset keypress: shift" end
+
+ bulk_menu:AddOption(L"Bulk select info: "..info, function() end):SetImage(pace.MiscIcons.info)
+ bulk_menu:AddOption(L"Bulk select clipboard info: " .. #pace.BulkSelectClipboard .. " copied parts", function() end):SetImage(pace.MiscIcons.info)
+
+ bulk_menu:AddOption(L"Insert (Move / Cut + Paste)", function()
+ pace.BulkCutPaste(obj)
+ end):SetImage('icon16/arrow_join.png')
+
+ bulk_menu:AddOption(L"Copy to Bulk Clipboard", function()
+ pace.BulkCopy(obj)
+ end):SetImage(pace.MiscIcons.copy)
+
+ bulk_menu:AddSpacer()
+
+ --bulk paste modes
+ bulk_menu:AddOption(L"Bulk Paste (bulk select -> into this part)", function()
+ pace.BulkPasteFromBulkSelectToSinglePart(obj)
+ end):SetImage('icon16/arrow_join.png')
+
+ bulk_menu:AddOption(L"Bulk Paste (clipboard or this part -> into bulk selection)", function()
+ if not pace.Clipboard then pace.Copy(obj) end
+ pace.BulkPasteFromSingleClipboard()
+ end):SetImage('icon16/arrow_divide.png')
+
+ bulk_menu:AddOption(L"Bulk Paste (Single paste from bulk clipboard -> into this part)", function()
+ pace.BulkPasteFromBulkClipboard(obj)
+ end):SetImage('icon16/arrow_join.png')
+
+ bulk_menu:AddOption(L"Bulk Paste (Multi-paste from bulk clipboard -> into bulk selection)", function()
+ for _,v in ipairs(pace.BulkSelectList) do
+ pace.BulkPasteFromBulkClipboard(v)
+ end
+ end):SetImage('icon16/arrow_divide.png')
+
+ bulk_menu:AddSpacer()
+
+ bulk_menu:AddOption(L"Bulk paste properties from selected part", function()
+ pace.Copy(obj)
+ for _,v in ipairs(pace.BulkSelectList) do
+ pace.PasteProperties(v)
+ end
+ end):SetImage(pace.MiscIcons.replace)
+
+ bulk_menu:AddOption(L"Bulk paste properties from clipboard", function()
+ for _,v in ipairs(pace.BulkSelectList) do
+ pace.PasteProperties(v)
+ end
+ end):SetImage(pace.MiscIcons.replace)
+
+ bulk_menu:AddSpacer()
+
+ bulk_menu:AddOption(L"Bulk Delete", function()
+ pace.BulkRemovePart()
+ end):SetImage(pace.MiscIcons.clear)
+
+ bulk_menu:AddOption(L"Clear Bulk List", function()
+ pace.ClearBulkList()
+ end):SetImage('icon16/table_delete.png')
+
+ menu:AddSpacer()
+ end]]
+
+ --pace.AddRegisteredPartsToMenu(menu, not obj)
+
+ --menu:AddSpacer()
+
+ --[[if obj then
+ local save, pnl = menu:AddSubMenu(L"save", function() pace.SaveParts() end)
+ pnl:SetImage(pace.MiscIcons.save)
+ add_expensive_submenu_load(pnl, function() pace.AddSaveMenuToMenu(save, obj) end)
+ end]]
+
+ --[[local load, pnl = menu:AddSubMenu(L"load", function() pace.LoadParts() end)
+ add_expensive_submenu_load(pnl, function() pace.AddSavedPartsToMenu(load, false, obj) end)
+ pnl:SetImage(pace.MiscIcons.load)]]
+
+ --[[if obj then
+ menu:AddSpacer()
+ menu:AddOption(L"remove", function() pace.RemovePart(obj) end):SetImage(pace.MiscIcons.clear)
+ end]]
+
+ menu:Open()
+ menu:MakePopup()
+ end
+
+ function pace.OnNewPartMenu()
+ pace.current_part = NULL
+ local menu = DermaMenu()
+
+ menu:MakePopup()
+ menu:SetPos(input.GetCursorPos())
+
+ pace.AddRegisteredPartsToMenu(menu)
+
+ menu:AddSpacer()
+
+ local load, pnl = menu:AddSubMenu(L"load", function() pace.LoadParts() end)
+ pnl:SetImage(pace.MiscIcons.load)
+ add_expensive_submenu_load(pnl, function() pace.AddSavedPartsToMenu(load, false, obj) end)
+ menu:AddOption(L"clear", function()
+ pace.ClearBulkList()
+ pace.ClearParts()
+ end):SetImage(pace.MiscIcons.clear)
+
+ end
+
+
+end
+
+function pace.GetPartSizeInformation(obj)
+ if not IsValid(obj) then return { raw_bytes = 0, info = "" } end
+ local charsize = #util.TableToJSON(obj:ToTable())
+ local root_charsize = #util.TableToJSON(obj:GetRootPart():ToTable())
+
+ local roots = {}
+ local all_charsize = 0
+ for i,v in pairs(pac.GetLocalParts()) do
+ roots[v:GetRootPart()] = v:GetRootPart()
+ end
+ for i,v in pairs(roots) do
+ all_charsize = all_charsize + #util.TableToJSON(v:ToTable())
+ end
+
+ return {
+ raw_bytes = charsize*2,
+ info = "raw JSON table size: " .. charsize*2 .. " bytes (" .. string.NiceSize(charsize*2) .. ")",
+ root_share_fraction = math.Round(charsize / root_charsize, 1),
+ root_share_percent = math.Round(100 * charsize / root_charsize, 1),
+ all_share_fraction = math.Round(charsize / all_charsize, 1),
+ all_share_percent = math.Round(100 * charsize / all_charsize, 1),
+ all_size_raw_bytes = all_charsize*2,
+ all_size_nice = string.NiceSize(all_charsize*2)
+ }
+end
+
+local part_classes_with_quicksetups = {
+ text = true,
+ particles = true,
+ proxy = true,
+ sprite = true,
+ projectile = true,
+ entity2 = true,
+ model2 = true,
+ group = true,
+ camera = true,
+ faceposer = true,
+ command = true,
+ bone3 = true,
+ health_modifier = true,
+ hitscan = true,
+ jiggle = true,
+ interpolated_multibone = true,
+}
+local function AddOptionRightClickable(title, func, parent_menu)
+ local pnl = parent_menu:AddOption(title, func)
+ function pnl:Think()
+ if input.IsMouseDown(MOUSE_RIGHT) and self:IsHovered() then
+ if not self.clicked then func() end
+ self.clicked = true
+ else
+ self.clicked = false
+ end
+ end
+ --to communicate to the user that they can right click to activate without closing the parent menu
+ --tooltip may be set later, so we'll sandwich it with a timer
+ timer.Simple(0.5, function()
+ if not IsValid(pnl) then return end
+ if pnl:GetTooltip() == nil then
+ pnl:SetTooltip("You can right click this to use the option without exiting the menu")
+ end
+ end)
+ return pnl
+end
+--those are more to configure a part into common setups, might involve creating other parts
+function pace.AddQuickSetupsToPartMenu(menu, obj)
+ if not part_classes_with_quicksetups[obj.ClassName] and not obj.GetDrawPosition then
+ return
+ end
+
+ local main, pnlmain = menu:AddSubMenu("quick setups") pnlmain:SetIcon("icon16/basket_go.png")
+ --base_movables can restructure, but nah bones aint it
+ if obj.GetDrawPosition and obj.ClassName ~= "bone" and obj.ClassName ~= "bone2" and obj.ClassName ~= "bone3" then
+ if obj.Bone and obj.Bone == "camera" then
+ main:AddOption("camera bone suggestion: limit view to yourself", function()
+ local event = pac.CreatePart("event") event:SetEvent("viewed_by_owner") event:SetParent(obj)
+ end):SetImage("icon16/star.png")
+ if obj.GetAlpha then main:AddOption("camera bone suggestion: fade with distance", function()
+ local model = pac.CreatePart("model2") model:SetModel("models/empty.mdl") model:SetParent(obj.Parent) model:SetName("head_position")
+ local proxy = pac.CreatePart("proxy") proxy:SetExpression("clamp(2 - (part_distance(\"head_position\")/100),0,1)") proxy:SetParent(obj) proxy:SetVariableName("Alpha")
+ end):SetImage("icon16/star.png") end
+ end
+ local substitutes, pnl = main:AddSubMenu("Restructure / Create parent substitute", function()
+ pace.SubstituteBaseMovable(obj, "create_parent")
+ timer.Simple(20, function() if pace.recently_substituted_movable_part == obj then pace.recently_substituted_movable_part = nil end end)
+ end) pnl:SetImage("icon16/application_double.png")
+ substitutes:AddOption("empty model", function()
+ --pulled from pace.SubstituteBaseMovable(obj, "create_parent")
+ local newObj = pac.CreatePart("model2")
+ if not IsValid(newObj) then return end
+
+ newObj:SetParent(obj.Parent)
+ obj:SetParent(newObj)
+
+ newObj:SetPosition(obj.Position)
+ newObj:SetPositionOffset(obj.PositionOffset)
+ newObj:SetAngles(obj.Angles)
+ newObj:SetAngleOffset(obj.AngleOffset)
+ newObj:SetEyeAngles(obj.EyeAngles)
+ newObj:SetAimPart(obj.AimPart)
+ newObj:SetAimPartName(obj.AimPartName)
+ newObj:SetBone(obj.Bone)
+ newObj:SetEditorExpand(true)
+ newObj:SetSize(0)
+ newObj:SetModel("models/empty.mdl")
+
+ obj:SetPosition(Vector(0,0,0))
+ obj:SetPositionOffset(Vector(0,0,0))
+ obj:SetAngles(Angle(0,0,0))
+ obj:SetAngleOffset(Angle(0,0,0))
+ obj:SetEyeAngles(false)
+ obj:SetAimPart(nil)
+ obj:SetAimPartName("")
+ obj:SetBone("head")
+
+ pace.RefreshTree()
+ end):SetIcon("icon16/anchor.png")
+ substitutes:AddOption("jiggle", function()
+ pace.SubstituteBaseMovable(obj, "create_parent", "jiggle")
+ end):SetIcon("icon16/chart_line.png")
+ substitutes:AddOption("interpolator", function()
+ pace.SubstituteBaseMovable(obj, "create_parent", "interpolated_multibone")
+ end):SetIcon("icon16/table_multiple.png")
+ end
+
+ local function install_submaterial_options(menu)
+ local mats = obj:GetOwner():GetMaterials()
+ local mats_str = table.concat(mats,"\n")
+ local dyn_props = obj:GetDynamicProperties()
+ local submat_togglers, pnl = main:AddSubMenu("create submaterial zone togglers (hide/show materials)", function()
+ Derma_StringRequest("submaterial togglers", "please input a submaterial name or a list of submaterial names with spaces\navailable materials:\n"..mats_str, "", function(str)
+ local event = pac.CreatePart("event") event:SetAffectChildrenOnly(true) event:SetEvent("command") event:SetArguments("materials_"..string.sub(obj.UniqueID,1,6))
+ local proxy = pac.CreatePart("proxy") proxy:SetAffectChildren(true) proxy:SetVariableName("no_draw") proxy:SetExpression("0") proxy:SetExpressionOnHide("1")
+ event:SetParent(obj) proxy:SetParent(event)
+ for i, kw in ipairs(string.Split(str, " ")) do
+ for id,mat2 in ipairs(mats) do
+ if string.GetFileFromFilename(mat2) == kw then
+ local mat = pac.CreatePart("material_3d") mat:SetParent(proxy)
+ mat:SetName("toggled_"..kw.."_"..string.sub(obj.UniqueID,1,6))
+ mat:SetLoadVmt(mat2)
+ dyn_props[kw].set("toggled_"..kw.."_"..string.sub(obj.UniqueID,1,6))
+ end
+ end
+ end
+ end)
+ end) pnl:SetImage("icon16/picture_delete.png") pnl:SetTooltip("The sub-options are right clickable")
+
+ local submat_toggler_proxy
+ local submat_toggler_event
+ local submaterials = {}
+ for i,mat2 in ipairs(mats) do
+ table.insert(submaterials,"")
+ local kw = string.GetFileFromFilename(mat2)
+ AddOptionRightClickable(kw, function()
+ if not submat_toggler_proxy then
+ local event = pac.CreatePart("event") event:SetAffectChildrenOnly(true) event:SetEvent("command") event:SetArguments("materials_"..string.sub(obj.UniqueID,1,6))
+ local proxy = pac.CreatePart("proxy") proxy:SetAffectChildren(true) proxy:SetVariableName("no_draw") proxy:SetExpression("0") proxy:SetExpressionOnHide("1")
+ event:SetParent(obj) proxy:SetParent(event)
+ submat_toggler_proxy = proxy
+ end
+ local mat = pac.CreatePart("material_3d") mat:SetParent(submat_toggler_proxy)
+ mat:SetName("toggled_"..kw.."_"..string.sub(obj.UniqueID,1,6))
+ mat:SetLoadVmt(mat2)
+
+ submaterials[i] = "toggled_"..kw.."_"..string.sub(obj.UniqueID,1,6)
+ if #submaterials == 1 then
+ obj:SetMaterials("") obj:SetMaterial(submaterials[1])
+ else
+ obj:SetMaterials(table.concat(submaterials, ";"))
+ end
+
+ end, submat_togglers):SetIcon("icon16/paintcan.png")
+ end
+
+ local edit_materials, pnl = main:AddSubMenu("edit all materials", function()
+ local materials = ""
+ obj:SetMaterial("")
+ for i,mat2 in ipairs(mats) do
+ local kw = string.GetFileFromFilename(mat2)
+ local mat = pac.CreatePart("material_3d") mat:SetParent(obj)
+ mat:SetName(kw.."_"..string.sub(obj.UniqueID,1,6))
+ mat:SetLoadVmt(mat2)
+ submaterials[i] = kw.."_"..string.sub(obj.UniqueID,1,6)
+
+ end
+ if #submaterials == 1 then
+ obj:SetMaterials("") obj:SetMaterial(submaterials[1])
+ else
+ obj:SetMaterials(table.concat(submaterials, ";"))
+ end
+ end) pnl:SetImage("icon16/paintcan.png")
+
+ for i,mat2 in ipairs(mats) do
+ local kw = string.GetFileFromFilename(mat2)
+ AddOptionRightClickable(kw, function()
+ obj:SetMaterial("")
+
+ local mat = pac.CreatePart("material_3d") mat:SetParent(obj)
+ mat:SetName(kw.."_"..string.sub(obj.UniqueID,1,6))
+ mat:SetLoadVmt(mat2)
+
+ submaterials[i] = kw.."_"..string.sub(obj.UniqueID,1,6)
+ if #submaterials == 1 then
+ obj:SetMaterials("") obj:SetMaterial(submaterials[1])
+ else
+ obj:SetMaterials(table.concat(submaterials, ";"))
+ end
+ end, edit_materials):SetIcon("icon16/paintcan.png")
+ end
+ end
+ if obj.ClassName == "particles" then
+ main:AddOption("bare 3D setup", function()
+ obj:Set3D(true) obj:SetZeroAngle(false) obj:SetVelocity(0) obj:SetParticleAngleVelocity(Vector(0,0,0)) obj:SetGravity(Vector(0,0,0))
+ end):SetIcon("icon16/star.png")
+ main:AddOption("simple 3D setup : Blast", function()
+ obj:Set3D(true) obj:SetZeroAngle(false) obj:SetLighting(false) obj:SetAngleOffset(Angle(90,0,0)) obj:SetStartSize(0) obj:SetEndSize(500) obj:SetFireOnce(true) obj:SetMaterial("particle/Particle_Ring_Wave_Additive") obj:SetVelocity(0) obj:SetParticleAngleVelocity(Vector(0,0,0)) obj:SetGravity(Vector(0,0,0)) obj:SetDieTime(1.5)
+ end):SetIcon("icon16/transmit.png")
+ main:AddOption("simple 3D setup : Slash", function()
+ obj:Set3D(true) obj:SetZeroAngle(false) obj:SetLighting(false) obj:SetAngleOffset(Angle(90,0,0)) obj:SetStartSize(100) obj:SetEndSize(90) obj:SetFireOnce(true) obj:SetMaterial("particle/Particle_Crescent") obj:SetVelocity(0) obj:SetParticleAngleVelocity(Vector(0,0,1500)) obj:SetGravity(Vector(0,0,0)) obj:SetDieTime(0.4)
+ end):SetIcon("icon16/arrow_refresh.png")
+ main:AddOption("simple setup : Piercer", function()
+ obj:Set3D(false) obj:SetZeroAngle(false) obj:SetLighting(false) obj:SetStartSize(30) obj:SetEndSize(10) obj:SetEndLength(100) obj:SetEndLength(1000) obj:SetFireOnce(true) obj:SetVelocity(50) obj:SetDieTime(0.2)
+ end):SetIcon("icon16/asterisk_orange.png")
+ main:AddOption("simple setup : Twinkle cloud", function()
+ obj:Set3D(false) obj:SetZeroAngle(false) obj:SetLighting(false) obj:SetStartSize(10) obj:SetEndSize(0) obj:SetEndLength(0) obj:SetEndLength(0) obj:SetFireOnce(false) obj:SetVelocity(0) obj:SetDieTime(0.5) obj:SetNumberParticles(2) obj:SetFireDelay(0.03) obj:SetPositionSpread(50) obj:SetGravity(Vector(0,0,0)) obj:SetMaterial("sprites/light_ignorez")
+ end):SetIcon("icon16/weather_snow.png")
+ main:AddOption("simple setup : Dust cloud", function()
+ obj:Set3D(false) obj:SetZeroAngle(false) obj:SetLighting(false) obj:SetStartSize(60) obj:SetEndSize(100) obj:SetEndLength(0) obj:SetEndLength(0) obj:SetStartAlpha(100) obj:SetFireOnce(false) obj:SetVelocity(0) obj:SetDieTime(2) obj:SetNumberParticles(2) obj:SetFireDelay(0.03) obj:SetPositionSpread(100) obj:SetGravity(Vector(0,0,-20))
+ end):SetIcon("icon16/weather_clouds.png")
+ main:AddOption("simple setup : Dust kickup", function()
+ obj:Set3D(false) obj:SetZeroAngle(false) obj:SetLighting(false) obj:SetStartSize(10) obj:SetEndSize(15) obj:SetEndLength(0) obj:SetEndLength(0) obj:SetStartAlpha(100) obj:SetFireOnce(true) obj:SetSpread(0.8) obj:SetVelocity(100) obj:SetDieTime(2) obj:SetNumberParticles(10) obj:SetPositionSpread(1) obj:SetAirResistance(80) obj:SetGravity(Vector(0,0,-100))
+ end):SetIcon("icon16/weather_clouds.png")
+ elseif obj.ClassName == "sprite" then
+ main:AddOption("simple shockwave (will use " .. (obj.Size == 1 and "size 200" or "existing size " .. obj.Size) ..")", function()
+ local proxyAlpha = pac.CreatePart("proxy")
+ proxyAlpha:SetParent(obj)
+ proxyAlpha:SetVariableName("Alpha")
+ proxyAlpha:SetExpression("clamp(1 - timeex()^0.5,0,1)")
+ local proxySize = pac.CreatePart("proxy")
+ proxySize:SetParent(obj)
+ proxySize:SetVariableName("Size")
+ proxySize:SetExpression((obj.Size == 1 and 200 or obj.Size) .. " * clamp(timeex()^0.5,0,1)")
+ obj:SetNotes("showhidetest")
+
+ pace.FlashNotification("Hide and unhide the sprite to review its effects. An additional menu option will be provided for this.")
+ end):SetIcon("icon16/transmit.png")
+ main:AddOption("cross flare", function()
+ obj:SetSizeY(0.1)
+ local proxy1 = pac.CreatePart("proxy")
+ proxy1:SetParent(obj)
+ proxy1:SetVariableName("SizeY")
+ proxy1:SetExpression("0.15*clamp(1 - timeex()^0.5,0,1)")
+ local proxy1_size = pac.CreatePart("proxy")
+ proxy1_size:SetParent(obj)
+ proxy1_size:SetVariableName("Size")
+ proxy1_size:SetExpression("100 + 100*clamp(timeex()^0.5,0,1)")
+
+ local sprite2 = pac.CreatePart("sprite")
+ sprite2:SetSpritePath(obj:GetSpritePath())
+ sprite2:SetParent(obj)
+ sprite2:SetSizeX(0.1)
+ local proxy2 = pac.CreatePart("proxy")
+ proxy2:SetParent(sprite2)
+ proxy2:SetVariableName("SizeX")
+ proxy2:SetExpression("0.15*clamp(1 - timeex()^0.5,0,1)")
+ local proxy2_size = pac.CreatePart("proxy")
+ proxy2_size:SetParent(sprite2)
+ proxy2_size:SetVariableName("Size")
+ proxy2_size:SetExpression("100 + 100*clamp(timeex()^0.5,0,1)")
+ obj:SetNotes("showhidetest")
+ end):SetIcon("icon16/asterisk_yellow.png")
+ elseif obj.ClassName == "proxy" then
+ pnlmain:SetTooltip("remember you also have a preset library by right clicking on the expression field")
+ main:AddOption("basic feedback controller setup", function()
+ Derma_StringRequest("What should we call this controller variable?", "Type a name for the commands.\nThese number ranges would be appropriate for positions\nIf you make more, name them something different", "speed", function(str)
+ if str == "" then return end if str == " " then return end
+ local cmdforward = pac.CreatePart("command") cmdforward:SetParent(obj)
+ cmdforward:SetString("pac_proxy " .. str .. " 100")
+ local btn = pac.CreatePart("event") btn:SetEvent("button") btn:SetArguments("up") btn:SetParent(cmdforward)
+
+ local cmdback = pac.CreatePart("command") cmdback:SetParent(obj)
+ cmdback:SetString("pac_proxy " .. str .. " -100")
+ local btn = pac.CreatePart("event") btn:SetEvent("button") btn:SetArguments("down") btn:SetParent(cmdback)
+
+ local cmdneutral = pac.CreatePart("command") cmdneutral:SetParent(obj)
+ cmdneutral:SetString("pac_proxy " .. str .. " 0")
+ local btn = pac.CreatePart("event") btn:SetEvent("button") btn:SetArguments("up") btn:SetParent(cmdneutral) btn:SetInvert(false)
+ local btn = pac.CreatePart("event") btn:SetEvent("button") btn:SetArguments("down") btn:SetParent(cmdneutral) btn:SetInvert(false)
+
+ obj:SetExpression("feedback() + ftime()*command(\"".. str .. "\")")
+ end)
+ end):SetIcon("icon16/joystick.png")
+ main:AddOption("2D feedback controller setup", function()
+ Derma_StringRequest("What should we call this controller variable?", "Type a name for the commands.\nThese number ranges would be appropriate for positions\nIf you make more, name them something different", "speed", function(str)
+ if str == "" then return end if str == " " then return end
+ local cmdforward = pac.CreatePart("command") cmdforward:SetParent(obj)
+ cmdforward:SetString("pac_proxy " .. str .. "_x" .. " 100")
+ local btn = pac.CreatePart("event") btn:SetEvent("button") btn:SetArguments("up") btn:SetParent(cmdforward)
+
+ local cmdback = pac.CreatePart("command") cmdback:SetParent(obj)
+ cmdback:SetString("pac_proxy " .. str .. "_x" .. " -100")
+ local btn = pac.CreatePart("event") btn:SetEvent("button") btn:SetArguments("down") btn:SetParent(cmdback)
+
+ local cmdneutral = pac.CreatePart("command") cmdneutral:SetParent(obj)
+ cmdneutral:SetString("pac_proxy " .. str .. "_x" .. " 0")
+ local btn = pac.CreatePart("event") btn:SetEvent("button") btn:SetArguments("up") btn:SetParent(cmdneutral) btn:SetInvert(false)
+ local btn = pac.CreatePart("event") btn:SetEvent("button") btn:SetArguments("down") btn:SetParent(cmdneutral) btn:SetInvert(false)
+
+
+ local cmdright = pac.CreatePart("command") cmdright:SetParent(obj)
+ cmdright:SetString("pac_proxy " .. str .. "_y" .. " 100")
+ local btn = pac.CreatePart("event") btn:SetEvent("button") btn:SetArguments("right") btn:SetParent(cmdright)
+
+ local cmdleft = pac.CreatePart("command") cmdleft:SetParent(obj)
+ cmdleft:SetString("pac_proxy " .. str .. "_y" .. " -100")
+ local btn = pac.CreatePart("event") btn:SetEvent("button") btn:SetArguments("left") btn:SetParent(cmdleft)
+
+ local cmdneutral = pac.CreatePart("command") cmdneutral:SetParent(obj)
+ cmdneutral:SetString("pac_proxy " .. str .. "_y" .. " 0")
+ local btn = pac.CreatePart("event") btn:SetEvent("button") btn:SetArguments("left") btn:SetParent(cmdneutral) btn:SetInvert(false)
+ local btn = pac.CreatePart("event") btn:SetEvent("button") btn:SetArguments("right") btn:SetParent(cmdneutral) btn:SetInvert(false)
+ obj:SetExpression(
+ "feedback_x() + ftime()*command(\"".. str .. "_x\")"
+ .. "," ..
+ "feedback_y() + ftime()*command(\"".. str .. "_y\")"
+ )
+ end)
+ end):SetIcon("icon16/joystick.png")
+ main:AddOption("command feedback attractor setup (-100, -50, 0, 50, 100)", function()
+ Derma_StringRequest("What should we call this attractor?", "Type a name for the commands.\nThese number ranges would be appropriate for positions\nIf you make more, name them something different", "target_number", function(str)
+ if str == "" then return end if str == " " then return end
+ local demonstration_values = {-100, -50, 0, 50, 100}
+ for i,value in ipairs(demonstration_values) do
+ local test_cmd_part = pac.CreatePart("command") test_cmd_part:SetParent(obj)
+ test_cmd_part:SetString("pac_proxy " .. str .. " " .. value)
+ end
+ obj:SetExpression("feedback() + 3*ftime()*(command(\"".. str .. "\") - feedback())")
+ end)
+ end):SetIcon("icon16/calculator.png")
+ main:AddOption("variable attractor multiplier base +1/0/-1", function()
+ Derma_StringRequest("What should we call this attractor?", "Type a name for the commands.\nIf you make more, name them something different", "target_number", function(str)
+ if str == "" then return end if str == " " then return end
+ local demonstration_values = {-1, 0, 1}
+ for i,value in ipairs(demonstration_values) do
+ local test_cmd_part = pac.CreatePart("command") test_cmd_part:SetParent(obj)
+ test_cmd_part:SetString("pac_proxy " .. str .. " " .. value)
+ end
+ local outsourced_proxy = pac.CreatePart("proxy")
+ outsourced_proxy:SetParent(obj) outsourced_proxy:SetName(str)
+ outsourced_proxy:SetExpression("feedback() + 3*ftime()*(command(\"".. str .. "\") - feedback())")
+ outsourced_proxy:SetExtra1("feedback() + 3*ftime()*(command(\"".. str .. "\") - feedback())")
+ if obj.Expression == "" then
+ obj:SetExpression("var1(\"".. str .. "\")")
+ else
+ obj:SetExpression(obj.Expression .. " * var1(\"".. str .. "\")")
+ end
+ end)
+ end):SetIcon("icon16/calculator.png")
+ main:AddOption("smoothen (wrap into dynamic feedback attractor)", function()
+ obj:SetExpression("feedback() + 4*ftime()*((" .. obj.Expression .. ") - feedback())")
+ end):SetIcon("icon16/calculator.png")
+ main:AddOption("smoothen (make extra variable attractor)", function()
+ Derma_StringRequest("What should we call this attractor variable?", "Type a name for the attractor. It will be used somewhere else like var1(\"eased_function\") for example\nsuggestions from the active functions:\n"..table.concat(obj:GetActiveFunctions(),"\n"), "eased_function", function(str)
+ local new_proxy = pac.CreatePart("proxy") new_proxy:SetParent(obj.Parent)
+ new_proxy:SetExpression("feedback() + 4*ftime()*((" .. obj.Expression .. ") - feedback())")
+ new_proxy:SetName(str)
+ new_proxy:SetExtra1(new_proxy.Expression)
+ end)
+ end):SetIcon("icon16/calculator.png")
+ elseif obj.ClassName == "text" then
+ main:AddOption("fast proxy link", function()
+ obj:SetTextOverride("Proxy")
+ obj:SetConcatenateTextAndOverrideValue(true)
+ --add proxy
+ local proxy = pac.CreatePart("proxy")
+ proxy:SetParent(obj)
+ proxy:SetVariableName("DynamicTextValue")
+ pace.Call("PartSelected", proxy)
+ end):SetIcon("icon16/calculator_link.png")
+ main:AddOption("quick large 2D text", function()
+ obj:SetDrawMode("SurfaceText")
+ obj:SetFont("DermaLarge")
+ end):SetIcon("icon16/text_letter_omega.png")
+ main:AddOption("make HUD", function()
+ obj:SetBone("player_eyes")
+ obj:SetPosition(Vector(10,0,0))
+ obj:SetDrawMode("SurfaceText")
+ local newevent = pac.CreatePart("event")
+ newevent:SetParent(obj)
+ newevent:SetEvent("viewed_by_owner")
+ end):SetIcon("icon16/monitor.png")
+ elseif obj.ClassName == "projectile" then
+ if obj.OutfitPartUID ~= "" then
+ local modelpart = obj.OutfitPart
+ if not modelpart.ClassName == "model2" then
+ modelpart = modelpart:GetChildren()[1]
+ if not modelpart.ClassName == "model2" then
+ return
+ end
+ end
+ if not modelpart.Model then return end
+ if obj.FallbackSurfpropModel ~= modelpart.Model then
+ main:AddOption("Reshape outfit part into a throwable prop: " .. obj.FallbackSurfpropModel, function()
+ obj:SetOverridePhysMesh(true)
+ obj:SetPhysical(true)
+ obj:SetRescalePhysMesh(true)
+ obj:SetRadius(modelpart.Size)
+ obj:SetFallbackSurfpropModel(modelpart.Model)
+ modelpart:SetHide(true)
+ end):SetIcon("materials/spawnicons/"..string.gsub(obj.FallbackSurfpropModel, ".mdl", "")..".png")
+ main:AddOption("Shape projectile into a throwable prop: " .. modelpart.Model, function()
+ obj:SetOverridePhysMesh(true)
+ obj:SetPhysical(true)
+ obj:SetRescalePhysMesh(true)
+ obj:SetFallbackSurfpropModel(modelpart.Model)
+ modelpart:SetHide(true)
+ modelpart:SetSize(obj.Radius)
+ modelpart:SetModel(obj.FallbackSurfpropModel)
+ end):SetIcon("materials/spawnicons/"..string.gsub(modelpart.Model, ".mdl", "")..".png")
+ end
+
+ else
+ if obj.FallbackSurfpropModel then
+ main:AddOption("make throwable prop (" .. obj.FallbackSurfpropModel .. ")", function()
+ local modelpart = pac.CreatePart("model2")
+ modelpart:SetParent(obj)
+ obj:SetOverridePhysMesh(true)
+ obj:SetPhysical(true)
+ obj:SetRescalePhysMesh(true)
+ obj:SetOutfitPart(modelpart)
+ modelpart:SetHide(true)
+ modelpart:SetSize(obj.Radius)
+ modelpart:SetModel(obj.FallbackSurfpropModel)
+ end):SetIcon("materials/spawnicons/"..string.gsub(obj.FallbackSurfpropModel, ".mdl", "")..".png")
+ end
+ main:AddOption("make throwable prop (opens asset browser)", function()
+ local modelpart = pac.CreatePart("model2")
+ modelpart:SetParent(obj)
+ obj:SetRadius(1)
+ obj:SetOverridePhysMesh(true)
+ obj:SetPhysical(true)
+ obj:SetRescalePhysMesh(true)
+ obj:SetOutfitPart(modelpart)
+ modelpart:SetHide(true)
+ pace.AssetBrowser(function(path)
+ modelpart:SetModel(path)
+ obj:SetFallbackSurfpropModel(path)
+ end, "models")
+ end):SetIcon("icon16/link.png")
+ end
+ main:AddOption("make shield", function()
+ local model = pac.CreatePart("model2") model:SetModel("models/props_lab/blastdoor001c.mdl")
+ model:SetParent(obj)
+ obj:SetPosition(Vector(60,0,0))
+ obj:SetRadius(1)
+ obj:SetSpeed(0)
+ obj:SetMass(10000)
+ obj:SetBone("invalidbone")
+ obj:SetOverridePhysMesh(true)
+ obj:SetPhysical(true)
+ obj:SetOutfitPart(model)
+ obj:SetCollideWithOwner(true)
+ obj:SetCollideWithSelf(true)
+ obj:SetFallbackSurfpropModel("models/props_lab/blastdoor001c.mdl")
+ model:SetHide(true)
+ pace.PopulateProperties(obj)
+ end):SetIcon("icon16/shield.png")
+
+ elseif obj.ClassName == "entity2" then
+ if obj:GetOwner().GetBodyGroups then
+ local bodygroups = obj:GetOwner():GetBodyGroups()
+ if #bodygroups > 0 then
+ local submenu, pnl = main:AddSubMenu("toggleable bodygroup with a dual proxy") pnl:SetImage("icon16/table_refresh.png")
+ pnl:SetTooltip("It will apply 1 and 0. But if there are more variations in that bodygroup, change the expression and the expression on hide if you wish")
+ for i,bodygroup in ipairs(bodygroups) do
+ if bodygroup.num == 1 then continue end
+ local pnl = submenu:AddOption(bodygroup.name, function()
+ local proxy = pac.CreatePart("proxy") proxy:SetParent(obj)
+ proxy:SetExpression("1") proxy:SetExpressionOnHide("0")
+ proxy:SetVariableName(bodygroup.name)
+ local event = pac.CreatePart("event") event:SetParent(proxy) event:SetEvent("command") event:SetArguments(string.Replace(bodygroup.name, " "))
+ end)
+ pnl:SetTooltip(table.ToString(bodygroup.submodels, nil, true))
+ end
+ end
+ end
+ install_submaterial_options(main)
+
+ elseif obj.ClassName == "model2" then
+ local pm = pace.current_part:GetPlayerOwner():GetModel()
+ local pm_selected = player_manager.TranslatePlayerModel(GetConVar("cl_playermodel"):GetString())
+
+ if pm_selected ~= pm then
+ main:AddOption("Selected playermodel - " .. string.gsub(string.GetFileFromFilename(pm_selected), ".mdl", ""), function()
+ obj:SetModel(pm_selected)
+ obj.pace_properties["Model"]:SetValue(pm_selected)
+ pace.PopulateProperties(obj)
+
+ end):SetImage("materials/spawnicons/"..string.gsub(pm_selected, ".mdl", "")..".png")
+ end
+ main:AddOption("Active playermodel - " .. string.gsub(string.GetFileFromFilename(pm), ".mdl", ""), function()
+ pace.current_part:SetModel(pm)
+ pace.current_part.pace_properties["Model"]:SetValue(pm)
+ pace.PopulateProperties(obj)
+
+ end):SetImage("materials/spawnicons/"..string.gsub(pm, ".mdl", "")..".png")
+ if IsValid(pac.LocalPlayer:GetActiveWeapon()) then
+ local wep = pac.LocalPlayer:GetActiveWeapon()
+ local wep_mdl = wep:GetModel()
+ if wep:GetClass() ~= "none" then --the uh hands have no model
+ main:AddOption("Active weapon - " .. wep:GetClass() .. " - model - " .. string.gsub(string.GetFileFromFilename(wep_mdl), ".mdl", ""), function()
+ obj:SetModel(wep_mdl)
+ obj.pace_properties["Model"]:SetValue(wep_mdl)
+ pace.PopulateProperties(obj)
+ end):SetImage("materials/spawnicons/"..string.gsub(wep_mdl, ".mdl", "")..".png")
+ end
+ end
+ if obj.Owner.GetBodyGroups then
+ local bodygroups = obj.Owner:GetBodyGroups()
+ if (#bodygroups > 1) or (#bodygroups[1].submodels > 1) then
+ local submenu, pnl = main:AddSubMenu("toggleable bodygroup with a dual proxy") pnl:SetImage("icon16/table_refresh.png")
+ pnl:SetTooltip("It will apply 1 and 0. But if there are more variations in that bodygroup, change the expression and the expression on hide if you wish")
+ for i,bodygroup in ipairs(bodygroups) do
+ if bodygroup.num == 1 then continue end
+ local pnl = submenu:AddOption(bodygroup.name, function()
+ local proxy = pac.CreatePart("proxy") proxy:SetParent(obj)
+ proxy:SetExpression("1") proxy:SetExpressionOnHide("0")
+ proxy:SetVariableName(bodygroup.name)
+ local event = pac.CreatePart("event") event:SetParent(proxy) event:SetEvent("command") event:SetArguments(string.Replace(bodygroup.name, " "))
+ end)
+ pnl:SetTooltip(table.ToString(bodygroup.submodels, nil, true))
+ end
+ end
+ end
+
+ install_submaterial_options(main)
+
+ local collapses, pnl = main:AddSubMenu("bone collapsers") pnl:SetImage("icon16/compress.png")
+ collapses:AddOption("collapse arms", function()
+ local group = pac.CreatePart("group") group:SetParent(obj)
+ local right = pac.CreatePart("bone3") right:SetParent(group) right:SetSize(0) right:SetScaleChildren(true) right:SetBone("right clavicle")
+ local left = pac.CreatePart("bone3") left:SetParent(group) left:SetSize(0) left:SetScaleChildren(true) left:SetBone("left clavicle")
+ end):SetIcon("icon16/user.png")
+ collapses:AddOption("collapse legs", function()
+ local group = pac.CreatePart("group") group:SetParent(obj)
+ local right = pac.CreatePart("bone3") right:SetBone("left thigh")
+ right:SetParent(group) right:SetSize(0) right:SetScaleChildren(true) right:SetBone("right thigh")
+ local left = pac.CreatePart("bone3") left:SetParent(group) left:SetSize(0) left:SetScaleChildren(true) left:SetBone("left thigh")
+ end):SetIcon("icon16/user.png")
+ collapses:AddOption("collapse by keyword", function()
+ Derma_StringRequest("collapse bones", "please input a keyword to match", "head", function(str)
+ local group = pac.CreatePart("group") group:SetParent(obj)
+ local ent = obj:GetOwner()
+ for bone,tbl in pairs(pac.GetAllBones(ent)) do
+ if string.find(bone, str) ~= nil then
+ local newbone = pac.CreatePart("bone3") newbone:SetParent(group) newbone:SetSize(0) newbone:SetScaleChildren(true) newbone:SetBone(bone)
+ end
+ end
+ end)
+ end):SetIcon("icon16/text_align_center.png")
+
+ elseif obj.ClassName == "group" then
+ main:AddOption("Assign to viewmodel", function()
+ obj:SetParent()
+ obj:SetOwnerName("viewmodel")
+ pace.RefreshTree(true)
+ end):SetIcon("icon16/user.png")
+ main:AddOption("Assign to hands", function()
+ obj:SetParent()
+ obj:SetOwnerName("hands")
+ pace.RefreshTree(true)
+ end):SetIcon("icon16/user.png")
+ main:AddOption("Assign to active vehicle", function()
+ obj:SetParent()
+ obj:SetOwnerName("active vehicle")
+ pace.RefreshTree(true)
+ end):SetIcon("icon16/user.png")
+ main:AddOption("Assign to active weapon", function()
+ obj:SetParent()
+ obj:SetOwnerName("active weapon")
+ pace.RefreshTree(true)
+ end):SetIcon("icon16/user.png")
+ main:AddOption("gather arm parts into hands", function()
+ if #obj:GetChildrenList() == 0 then return end
+ local gatherable_classes = {
+ model2 = true,
+ model = true,
+ }
+ local groupable_classes = {
+ group = true,
+ event = true,
+ }
+ local newgroup = pac.CreatePart("group")
+ local function ProcessDrawablePartsRecursively(part, root)
+ if gatherable_classes[part.ClassName] then
+ if not (string.find(part.Bone, "hand") ~= nil or string.find(part.Bone, "upperarm") ~= nil or string.find(part.Bone, "forearm") ~= nil
+ or string.find(part.Bone, "wrist") ~= nil or string.find(part.Bone, "ulna") ~= nil or string.find(part.Bone, "bicep") ~= nil
+ or string.find(part.Bone, "finger") ~= nil)
+ then
+ part:Remove()
+ end
+ elseif groupable_classes[part.ClassName] then
+ for i, child in ipairs(part:GetChildrenList()) do
+ ProcessDrawablePartsRecursively(child, root)
+ end
+ else
+ part:Remove()
+ end
+ end
+ pace.Copy(obj)
+ pace.Paste(newgroup)
+ ProcessDrawablePartsRecursively(newgroup, newgroup)
+
+ newgroup:SetOwnerName("hands")
+ newgroup:SetName("[HANDS]")
+ pace.RefreshTree(true)
+ end):SetIcon("icon16/user.png")
+ elseif obj.ClassName == "camera" then
+ menu:AddOption("clone position as a node for interpolators", function()
+ local newpart = pac.CreatePart("model2")
+ newpart:SetParent(obj:GetParent())
+ newpart:SetModel("models/editor/camera.mdl") newpart:SetMaterial("models/wireframe")
+ newpart:SetPosition(obj:GetPosition())
+ newpart:SetPositionOffset(obj:GetPositionOffset())
+ newpart:SetAngles(obj:GetAngles())
+ newpart:SetAngleOffset(obj:GetAngleOffset())
+ newpart:SetBone(obj:GetBone())
+ newpart:SetNotes("editor FOV: " .. math.Round(pace.ViewFOV,1) ..
+ "\ncamera FOV: " .. math.Round(obj:GetFOV(),1) ..
+ (obj.Name ~= "" and ("\ncamera name: " .. obj:GetName()) or "") ..
+ "\ncamera UID: " .. obj.UniqueID
+ )
+ Derma_StringRequest("Set a name", "give a name to the camera position node", "camera_node", function(str)
+ newpart:SetName(str)
+ if newpart.pace_tree_node then
+ newpart.pace_tree_node:SetText(str)
+ end
+ end)
+ end):SetImage("icon16/find.png")
+
+ local bone_parent = obj:GetParent()
+ if obj:GetOwner() ~= obj:GetRootPart():GetOwner() then
+ while not (bone_parent.Bone and bone_parent.GetWorldPosition) do
+ bone_parent = bone_parent:GetParent()
+ if bone_parent:GetOwner() == obj:GetRootPart():GetOwner() then
+ bone_parent = obj:GetRootPart():GetOwner()
+ end
+ end
+ else
+ bone_parent = obj:GetRootPart():GetOwner()
+ end
+ local function bone_reposition(bone)
+ local bone_pos, bone_ang = pac.GetBonePosAng(obj:GetOwner(), bone)
+ local pos, ang = WorldToLocal(pace.ViewPos, pace.view_roll and pace.ViewAngles_postRoll or pace.ViewAngles, bone_pos, bone_ang)
+ obj:SetPosition(pos) obj:SetAngles(ang) obj:SetEyeAnglesLerp(0) obj:SetBone(bone)
+ pace.PopulateProperties(obj)
+ end
+ local translate_from_view, pnl = main:AddSubMenu("Apply editor view", function()
+ bone_reposition(obj.Bone)
+ end) pnl:SetImage("icon16/arrow_redo.png")
+
+ AddOptionRightClickable("apply FOV: " .. math.Round(pace.ViewFOV,1), function()
+ obj:SetFOV(math.Round(pace.ViewFOV,1))
+ pace.PopulateProperties(obj)
+ end, translate_from_view):SetImage("icon16/zoom.png")
+
+ AddOptionRightClickable("reset FOV" , function()
+ obj:SetFOV(-1)
+ pace.PopulateProperties(obj)
+ end, translate_from_view):SetImage("icon16/zoom_out.png")
+
+ translate_from_view:AddOption("current bone: " .. obj.Bone, function()
+ bone_reposition(obj.Bone)
+ end):SetImage("icon16/arrow_redo.png")
+ translate_from_view:AddOption("no bone", function()
+ bone_reposition("invalidbone")
+ end):SetImage("icon16/arrow_redo.png")
+
+ local bone_list = {}
+ if isentity(bone_parent) then
+ bone_list = pac.GetAllBones(bone_parent)
+ else
+ bone_list = pac.GetAllBones(bone_parent:GetOwner())
+ end
+ local bonekeys = {}
+ local common_human_bones = {
+ "head", "neck", "spine", "spine 1", "spine 2", "spine 4", "pelvis",
+ "left clavicle", "left upperarm", "left forearm", "left hand",
+ "right clavicle", "right upperarm", "right forearm", "right hand",
+ "left thigh", "left calf", "left foot", "left toe", "right thigh", "right calf", "right foot", "right toe"
+ }
+ local sorted_bonekeys = {}
+ for i,v in pairs(bone_list) do
+ bonekeys[v.friendly] = v.friendly
+ end
+ for i,v in SortedPairs(bonekeys) do
+ table.insert(sorted_bonekeys, v)
+ end
+
+ --basic humanoid bones
+ if bone_list["spine"] and bone_list["head"] and bone_list["left upperarm"] and bone_list["right upperarm"] then
+ local common_bones_menu, pnl = translate_from_view:AddSubMenu("shortened bone list (humanoid)") pnl:SetIcon("icon16/user.png")
+ for _,bonename in ipairs(common_human_bones) do
+ AddOptionRightClickable(bonename, function()
+ bone_reposition(bonename)
+ end, common_bones_menu):SetImage("icon16/user.png")
+ end
+ end
+
+ translate_from_view:AddSpacer()
+ local full_bones_menu, pnl = translate_from_view:AddSubMenu("full bone list for " .. tostring(bone_parent)) pnl:SetIcon("icon16/user_add.png")
+ --full bone list
+ for _,bonename in ipairs(sorted_bonekeys) do
+ AddOptionRightClickable(bonename, function()
+ bone_reposition(bonename)
+ end, full_bones_menu):SetImage("icon16/connect.png")
+ end
+
+ local function extract_camera_from_jiggle()
+ camera = obj
+ if not IsValid(camera.recent_jiggle) then
+ return
+ end
+ local jig = camera.recent_jiggle
+ local camang = jig:GetAngles()
+ local campos = jig:GetPosition()
+ local cambone = jig:GetBone()
+ local camparent = jig:GetParent()
+ camera:SetParent(camparent)
+ camera:SetBone(cambone) camera:SetAngles(camang) camera:SetPosition(campos)
+ jig:Remove()
+ if not camera:IsHidden() then
+ if not camera.Hide then
+ timer.Simple(0, function()
+ camera:SetHide(true) camera:SetHide(false)
+ end)
+ end
+ end
+ end
+ local function insert_camera_into_jiggle()
+ camera = obj
+ local jig = camera.recent_jiggle
+ if not IsValid(camera.recent_jiggle) then
+ jig = pac.CreatePart("jiggle")
+ camera.recent_jiggle = jig
+ jig:SetEditorExpand(true)
+ end
+ jig:SetParent(camera:GetParent())
+ jig:SetBone(camera.Bone) jig:SetAngles(camera:GetAngles()) jig:SetPosition(camera:GetPosition())
+ camera:SetBone("head") camera:SetAngles(Angle(0,0,0)) camera:SetPosition(Vector(0,0,0))
+ camera:SetParent(jig)
+ if not camera:IsHidden() then
+ if not camera.Hide then
+ timer.Simple(0, function()
+ camera:SetHide(true) camera:SetHide(false)
+ end)
+ end
+ end
+
+ return jig
+ end
+
+ --helper variable to adjust relative to player height
+ local ent = obj:GetRootPart():GetOwner()
+ local default_headbone = ent:LookupBone("ValveBiped.Bip01_Head1")
+ if not default_headbone then
+ for i=0,ent:GetBoneCount(),1 do
+ if string.find(ent:GetBoneName(i), "head") or string.find(ent:GetBoneName(i), "Head") then
+ default_headbone = i
+ break
+ end
+ end
+ end
+
+ if default_headbone then
+ local head_base_pos = ent:GetBonePosition(default_headbone)
+ local trace = util.QuickTrace(head_base_pos + Vector(0,0,50), Vector(0,0,-10000), function(ent2) return ent2 == ent end)
+ local mins, maxs = ent:GetHull()
+
+ local height_headbase = math.Round((head_base_pos - ent:GetPos()).z,1)
+ local height_eyepos = math.Round((ent:EyePos() - ent:GetPos()).z,1)
+ local height_traced = math.Round((trace.HitPos - ent:GetPos()).z,1)
+ local height_hull = (maxs - mins).z
+
+ local height = height_traced
+ if trace.Entity ~= ent then
+ height = height_headbase
+ end
+
+ local info, pnl = main:AddSubMenu("calculated head height : " .. height .. " HU (" .. math.Round(height / 39,2) .." m)")
+ info:AddOption("alternate height calculations"):SetImage("icon16/help.png")
+ info:SetTooltip("Due to lack of standardization on models' scales, heights are not guaranteed to be accurate or consistent\n\nThe unit conversion used is 1 Hammer Unit : 2.5 cm (1 inch)")
+ info:AddSpacer()
+
+ AddOptionRightClickable("head bone's base position : " .. height_headbase .. " HU (" .. math.Round(height_headbase / 39,2) .." m)", function()
+ height = height_headbase
+ pnl:SetText("calculated head height : " .. height .. " HU (" .. math.Round(height / 39,2) .." m)")
+ end, info):SetIcon("icon16/monkey.png")
+ AddOptionRightClickable("traced to top of the head: " .. height_traced .. " HU (" .. math.Round(height_traced / 39,2) .." m)", function()
+ height = height_traced
+ pnl:SetText("calculated head height : " .. height .. " HU (" .. math.Round(height / 39,2) .." m)")
+ end, info):SetIcon("icon16/arrow_down.png")
+ AddOptionRightClickable("player eye position (ent:EyePos()) : " .. height_eyepos .. " HU (" .. math.Round(height_eyepos / 39,2) .." m)", function()
+ height = height_eyepos
+ pnl:SetText("calculated head height : " .. height .. " HU (" .. math.Round(height / 39,2) .." m)")
+ end, info):SetIcon("icon16/eye.png")
+ AddOptionRightClickable("hull dimensions : " .. height_hull .. " HU (" .. math.Round(height_hull / 39,2) .." m)", function()
+ height = height_hull
+ pnl:SetText("calculated head height : " .. height .. " HU (" .. math.Round(height / 39,2) .." m)")
+ end, info):SetIcon("icon16/collision_on.png")
+
+ pnl:SetImage("icon16/help.png")
+ pnl:SetTooltip(ent:GetBoneName(default_headbone) .. "\n" .. ent:GetModel())
+ local fp, pnl = main:AddSubMenu("first person camera setups") pnl:SetImage("icon16/eye.png")
+ AddOptionRightClickable("easy first person (head)", function()
+ extract_camera_from_jiggle()
+ obj:SetBone("head")
+ obj:SetPosition(Vector(5,-4,0)) obj:SetEyeAnglesLerp(1) obj:SetAngles(Angle(0,-90,-90))
+ pace.PopulateProperties(obj)
+ end, fp):SetIcon("icon16/eye.png")
+
+ AddOptionRightClickable("on neck + collapsed head", function()
+ extract_camera_from_jiggle()
+ obj:SetBone("neck")
+ obj:SetPosition(Vector(5,0,0)) obj:SetEyeAnglesLerp(1) obj:SetAngles(Angle(0,-90,-90))
+ local bone = pac.CreatePart("bone3")
+ bone:SetScaleChildren(true) bone:SetSize(0)
+ bone:SetParent(obj)
+ local event = pac.CreatePart("event") event:SetEvent("viewed_by_owner") event:SetParent(bone)
+ pace.PopulateProperties(obj)
+ end, fp):SetIcon("icon16/eye.png")
+
+ AddOptionRightClickable("on neck + collapsed head + eyeang limiter", function()
+ extract_camera_from_jiggle()
+ obj:SetBone("neck")
+ obj:SetPosition(Vector(5,0,0)) obj:SetEyeAnglesLerp(0.7) obj:SetAngles(Angle(0,-90,-90))
+ local bone = pac.CreatePart("bone3")
+ bone:SetScaleChildren(true) bone:SetSize(0)
+ bone:SetParent(obj)
+ local event = pac.CreatePart("event") event:SetEvent("viewed_by_owner") event:SetParent(bone)
+ pace.PopulateProperties(obj)
+ end, fp):SetIcon("icon16/eye.png")
+
+ AddOptionRightClickable("smoothen", function()
+ insert_camera_into_jiggle()
+ pace.PopulateProperties(obj)
+ end, main):SetIcon("icon16/chart_line.png")
+ AddOptionRightClickable("undo smoothen (extract from jiggle)", function()
+ extract_camera_from_jiggle()
+ pace.PopulateProperties(obj)
+ end, main):SetIcon("icon16/chart_line_delete.png")
+
+ AddOptionRightClickable("close up (zoomed on the face)", function()
+ extract_camera_from_jiggle()
+ obj:SetBone("head") obj:SetAngles(Angle(0,90,90)) obj:SetPosition(Vector(3,-20,0)) obj:SetEyeAnglesLerp(0) obj:SetFOV(45)
+ pace.PopulateProperties(obj)
+ end, main):SetIcon("icon16/monkey.png")
+
+ AddOptionRightClickable("Cowboy / medium shot (waist up) (relative to neck)", function()
+ extract_camera_from_jiggle()
+ obj:SetBone("neck") obj:SetAngles(Angle(0,120,90)) obj:SetPosition(Vector(14,-24,0)) obj:SetEyeAnglesLerp(0) obj:SetFOV(-1)
+ pace.PopulateProperties(obj)
+ end, main):SetIcon("icon16/user.png")
+
+ AddOptionRightClickable("Cowboy / medium shot (waist up) (no bone) (20 + 0.6*height)", function()
+ extract_camera_from_jiggle()
+ obj:SetBone("invalidbone") obj:SetAngles(Angle(0,180,0)) obj:SetPosition(Vector(40,0,20 + 0.6*height)) obj:SetEyeAnglesLerp(0) obj:SetFOV(-1)
+ pace.PopulateProperties(obj)
+ end, main):SetIcon("icon16/user.png")
+
+ AddOptionRightClickable("over the shoulder (no bone) (12 + 0.8*height)", function()
+ extract_camera_from_jiggle()
+ obj:SetBone("invalidbone") obj:SetAngles(Angle(0,0,0)) obj:SetPosition(Vector(-30,15,12 + 0.8*height)) obj:SetEyeAnglesLerp(0.3) obj:SetFOV(-1)
+ pace.PopulateProperties(obj)
+ end, main):SetIcon("icon16/user_gray.png")
+
+ AddOptionRightClickable("over the shoulder (with jiggle)", function()
+ local jiggle = insert_camera_into_jiggle()
+ jiggle:SetConstrainSphere(75) jiggle:SetSpeed(3)
+ obj:SetEyeAnglesLerp(0.7) obj:SetFOV(-1)
+ jiggle:SetBone("neck") jiggle:SetAngles(Angle(180,90,90)) jiggle:SetPosition(Vector(-2,18,-10))
+ pace.PopulateProperties(obj)
+ end, main):SetIcon("icon16/user_gray.png")
+
+ AddOptionRightClickable("full shot (0.7*height)", function()
+ extract_camera_from_jiggle()
+ obj:SetEyeAnglesLerp(0) obj:SetFOV(-1)
+ obj:SetBone("invalidbone") obj:SetAngles(Angle(6,180,0)) obj:SetPosition(Vector(height,-15,height * 0.7))
+ pace.PopulateProperties(obj)
+ end, main):SetIcon("icon16/user_suit.png")
+ end
+
+ AddOptionRightClickable("wide shot (with jiggle)", function()
+ local jiggle = insert_camera_into_jiggle()
+ jiggle:SetConstrainSphere(150) jiggle:SetSpeed(1)
+ obj:SetEyeAnglesLerp(0.2) obj:SetFOV(-1)
+ jiggle:SetBone("invalidbone") jiggle:SetAngles(Angle(0,0,0)) jiggle:SetPosition(Vector(0,15,120))
+ obj:SetPosition(Vector(-250,0,0))
+ pace.PopulateProperties(obj)
+ end, main):SetIcon("icon16/arrow_out.png")
+
+ AddOptionRightClickable("extreme wide shot (with jiggle)", function()
+ local jiggle = insert_camera_into_jiggle()
+ jiggle:SetConstrainSphere(0) jiggle:SetSpeed(0.3)
+ obj:SetEyeAnglesLerp(0.1) obj:SetFOV(-1)
+ jiggle:SetBone("invalidbone") jiggle:SetAngles(Angle(0,0,0)) jiggle:SetPosition(Vector(-500,0,200))
+ obj:SetPosition(Vector(0,0,0)) obj:SetAngles(Angle(15,0,0))
+ pace.PopulateProperties(obj)
+ end, main):SetIcon("icon16/map.png")
+
+ AddOptionRightClickable("bird eye view (with jiggle)", function()
+ local jiggle = insert_camera_into_jiggle()
+ jiggle:SetConstrainSphere(300) jiggle:SetSpeed(1)
+ obj:SetEyeAnglesLerp(0.2) obj:SetFOV(-1)
+ jiggle:SetBone("invalidbone") jiggle:SetAngles(Angle(0,0,0)) jiggle:SetPosition(Vector(-150,0,300))
+ obj:SetPosition(Vector(0,0,0)) obj:SetAngles(Angle(70,0,0))
+ pace.PopulateProperties(obj)
+ end, main):SetIcon("icon16/map_magnify.png")
+
+ AddOptionRightClickable("Dutch shot (tilt)", function()
+ local jiggle = insert_camera_into_jiggle()
+ jiggle:SetConstrainSphere(150) jiggle:SetSpeed(1)
+ obj:SetEyeAnglesLerp(0) obj:SetFOV(-1)
+ jiggle:SetBone("invalidbone") jiggle:SetAngles(Angle(0,0,0)) jiggle:SetPosition(Vector(0,15,50))
+ obj:SetPosition(Vector(-75,0,0)) obj:SetAngles(Angle(0,0,25))
+ pace.PopulateProperties(obj)
+ end, main):SetIcon("icon16/arrow_refresh.png")
+ elseif obj.ClassName == "faceposer" then
+ if obj:GetDynamicProperties() == nil then main:AddOption("No flexes found!"):SetIcon("icon16/cancel.png") return end
+ main:AddOption("reset expressions", function()
+ for i,prop in pairs(obj:GetDynamicProperties()) do
+ if string.lower(prop.key) == prop.key or prop.key == "Blink" then
+ prop.set(obj,0)
+ end
+ end
+ pace.PopulateProperties(obj)
+ end):SetIcon("icon16/cancel.png")
+ local flexes = {}
+ for i,prop in pairs(obj:GetDynamicProperties()) do
+ flexes[prop.key] = prop.key
+ end
+
+ local function full_match(tbl)
+ for i,v in pairs(tbl) do
+ if not flexes[v] then
+ return false
+ end
+ end
+ return true
+ end
+ local common_combinations = {
+ {"eyes_look_down", "eyes_look_up", "eyes_look_right", "eyes_look_left"},
+ {"eyes-look-down", "eyes-look-up", "eyes-look-right", "eyes-look-left"},
+ {"eye_left_down", "eye_left_up", "eye_left_right", "eye_left_left", "eye_right_down", "eye_right_up", "eye_right_right", "eye_right_left"},
+ {"Eyes Down", "Eyes Up", "Eyes Right", "Eyes Left"},
+ {"eyes down", "eyes up", "eyes right", "eyes left"},
+ {"eye_down", "eye_up", "eye_right", "eye_left"},
+ {"LookDown", "LookUp", "LookRight", "LookLeft"}
+ }
+ local final_combination
+ for i,tbl in ipairs(common_combinations) do
+ if full_match(tbl) then
+ final_combination = tbl
+ end
+ end
+
+ if final_combination then
+ main:AddOption("4-way look", function()
+ for _, flex in ipairs(final_combination) do
+ local new_proxy = pac.CreatePart("proxy") new_proxy:SetParent(obj)
+ if string.match(string.lower(flex), "down$") then
+ new_proxy:SetExpression("pose_parameter_true(\"head_pitch\")/60")
+ elseif string.match(string.lower(flex), "up$") then
+ new_proxy:SetExpression("-pose_parameter_true(\"head_pitch\")/60")
+ elseif string.match(string.lower(flex), "left$") then
+ new_proxy:SetExpression("pose_parameter_true(\"head_yaw\")/30")
+ elseif string.match(string.lower(flex), "right$") then
+ new_proxy:SetExpression("-pose_parameter_true(\"head_yaw\")/30")
+ end
+ new_proxy:SetVariableName(flex)
+ end
+ end):SetIcon("icon16/calculator.png")
+ else --what if those are bones?
+
+ end
+
+ main:AddOption("add face camera and view it", function()
+ local cam = pac.CreatePart("camera") cam:SetParent(obj)
+ cam:SetBone("head") cam:SetAngles(Angle(0,90,90)) cam:SetPosition(Vector(3,-20,0)) cam:SetEyeAnglesLerp(0) cam:SetFOV(45)
+ pace.PopulateProperties(cam)
+ pace.ManuallySelectCamera(cam, true)
+ end):SetIcon("icon16/camera.png")
+ elseif obj.ClassName == "command" then
+ if pac.LocalPlayer.pac_command_events then
+ local cmd_menu, pnl = main:AddSubMenu("command event activators") pnl:SetImage("icon16/clock_red.png")
+ for cmd,_ in SortedPairs(pac.LocalPlayer.pac_command_events) do
+ cmd_menu2, pnl2 = cmd_menu:AddSubMenu(cmd) pnl2:SetImage("icon16/clock_red.png")
+ cmd_menu2:AddOption("instant", function()
+ obj:SetString("pac_event " .. cmd)
+ end):SetImage("icon16/clock_red.png")
+ cmd_menu2:AddOption("on", function()
+ obj:SetString("pac_event " .. cmd .. " 1")
+ end):SetImage("icon16/clock_red.png")
+ cmd_menu2:AddOption("off", function()
+ obj:SetString("pac_event " .. cmd .. " 0")
+ end):SetImage("icon16/clock_red.png")
+ cmd_menu2:AddOption("toggle", function()
+ obj:SetString("pac_event " .. cmd .. " 2")
+ end):SetImage("icon16/clock_red.png")
+ end
+
+ main:AddOption("save current events to a single command", function()
+ local tbl3 = {}
+ for i,v in pairs(pac.LocalPlayer.pac_command_events) do tbl3[i] = v.on end
+ new_expression = ""
+ for i,v in pairs(tbl3) do new_expression = new_expression .. "pac_event " .. i .. " " .. v .. ";" end
+ obj:SetUseLua(false)
+ end):SetIcon("icon16/application_xp_terminal.png")
+
+ end
+ local inputs = {"forward", "back", "moveleft", "moveright", "attack", "attack2", "use", "left", "right", "jump", "duck", "speed", "walk", "reload", "alt1", "alt2", "showscores", "grenade1", "grenade2"}
+ local input_menu, pnl = main:AddSubMenu("movement controllers (dash etc.)")
+ --standard blip
+ local input_menu1, pnl2 = input_menu:AddSubMenu("quick trigger") pnl2:SetImage("icon16/asterisk_yellow.png")
+ for i,mv in ipairs(inputs) do
+ input_menu1:AddOption(mv, function()
+ obj:SetString("+"..mv)
+ local timerx = pac.CreatePart("event") timerx:SetParent(obj) timerx:SetAffectChildrenOnly(true) timerx:SetEvent("timerx") timerx:SetArguments("0.2@@1@@0")
+ local off_cmd = pac.CreatePart("command") off_cmd:SetParent(timerx) off_cmd:SetString("-"..mv)
+ end):SetIcon("icon16/asterisk_yellow.png")
+ end
+ --button substitutor
+ local input_menu2, pnl2 = input_menu:AddSubMenu("button pair (fake bind)") pnl2:SetImage("icon16/contrast_high.png")
+ for i,mv in ipairs(inputs) do
+ input_menu2:AddOption(mv, function()
+ Derma_StringRequest("movement command setup", "write a button to use!", "mouse_left", function(str)
+ obj:SetString("+"..mv)
+ local newevent1 = pac.CreatePart("event") newevent1:SetEvent("button") newevent1:SetInvert(true) newevent1:SetArguments(str)
+ local newevent2 = pac.CreatePart("event") newevent2:SetEvent("button") newevent2:SetInvert(false) newevent2:SetArguments(str)
+ local off_cmd = pac.CreatePart("command") off_cmd:SetString("-"..mv)
+
+ off_cmd:SetParent(obj.Parent)
+ if obj.Parent.ClassName == "event" and obj.Parent.AffectChildrenOnly then
+ local parent = obj.Parent
+ newevent1:SetParent(parent)
+ newevent1:SetAffectChildrenOnly(true)
+ obj:SetParent(newevent1)
+ newevent2:SetParent(parent)
+ newevent2:SetAffectChildrenOnly(true)
+ off_cmd:SetParent(newevent2)
+ else
+ newevent1:SetParent(obj)
+ newevent2:SetParent(off_cmd)
+ off_cmd:SetParent(obj.Parent)
+ end
+ end)
+ end):SetIcon("icon16/contrast_high.png")
+ end
+ pnl:SetImage("icon16/keyboard.png")
+
+
+ local lua_menu, pnl = main:AddSubMenu("Lua hackery") pnl:SetImage("icon16/page_code.png")
+ lua_menu:AddOption("Chat decoder -> command proxy", function()
+ Derma_StringRequest("create chat decoder", "please input a name to use for the decoder.\ne.g. you will say \"value=5", "", function(str)
+ obj:SetUseLua(true) obj:SetString([[local strs = string.Split(LocalPlayer().pac_say_event.str, "=") RunConsoleCommand("pac_proxy", "]] .. str .. [[", tonumber(strs[2]))]])
+ local say = pac.CreatePart("event") say:SetEvent("say") say:SetInvert(true) say:SetArguments(str .. "=0.5") say:SetAffectChildrenOnly(true)
+ local timerx = pac.CreatePart("event") timerx:SetEvent("timerx") timerx:SetInvert(false) timerx:SetArguments("0.2@@1@@0") timerx:SetAffectChildrenOnly(true)
+ say:SetParent(obj.Parent) timerx:SetParent(say) obj:SetParent(say)
+ local proxy = pac.CreatePart("proxy") proxy:SetExpression("command(\"" .. str .. "\")")
+ proxy:SetParent(obj.Parent)
+ end)
+ end):SetIcon("icon16/comment.png")
+ lua_menu:AddOption("random command (e.g. trigger random animations)", function()
+ Derma_StringRequest("create random command", "please input a name for the event series\nyou should probably already have a series of command events like animation1, animation2, animation3 etc", "", function(str)
+ obj:SetUseLua(true) obj:SetString([[local num = math.floor(math.random()*5) RunConsoleCommand("pac_event", "]] .. str .. [[" num]])
+ end)
+ end):SetIcon("icon16/award_star_gold_1.png")
+ lua_menu:AddOption("random command (pac_proxy)", function()
+ Derma_StringRequest("create random command", "please input a name for the proxy command", "", function(str)
+ obj:SetUseLua(true) obj:SetString([[local num = math.random()*100 RunConsoleCommand("pac_proxy", "]] .. str .. [[" num]])
+ end)
+ end):SetIcon("icon16/calculator.png")
+ lua_menu:AddOption("X-Ray hook (halos)", function()
+ obj:SetName("halos on") obj:SetString([[hook.Add("PreDrawHalos","xray_halos", function() halo.Add(ents.FindByClass("npc_combine_s"), Color(255,0,0), 5, 5, 5, true, true) end)]])
+ local newobj = pac.CreatePart("command") newobj:SetParent(obj.Parent) newobj:SetName("halos off") newobj:SetString([[hook.Remove("PreDrawHalos","xray_halos")]])
+ obj:SetUseLua(true) newobj:SetUseLua(true)
+ end):SetIcon("icon16/shading.png")
+ lua_menu:AddOption("X-Ray hook (ignorez)", function()
+ obj:SetName("ignoreZ on") obj:SetString([[hook.Add("PostDrawTranslucentRenderables","xray_ignorez", function() cam.IgnoreZ( true ) for i,ent in pairs(ents.FindByClass("npc_combine_s")) do ent:DrawModel() end cam.IgnoreZ( false ) end)]])
+ local newobj = pac.CreatePart("command") newobj:SetName("ignoreZ off") newobj:SetParent(obj.Parent) newobj:SetString([[hook.Remove("PostDrawTranslucentRenderables","xray_ignorez")]])
+ obj:SetUseLua(true) newobj:SetUseLua(true)
+ end):SetIcon("icon16/shape_move_front.png")
+ elseif obj.ClassName == "bone3" then
+ local collapses, pnl = main:AddSubMenu("bone collapsers") pnl:SetImage("icon16/compress.png")
+ collapses:AddOption("collapse arms", function()
+ local group = pac.CreatePart("group") group:SetParent(obj.Parent)
+ local right = pac.CreatePart("bone3") right:SetParent(group) right:SetSize(0) right:SetScaleChildren(true) right:SetBone("right clavicle")
+ local left = pac.CreatePart("bone3") left:SetParent(group) left:SetSize(0) left:SetScaleChildren(true) left:SetBone("left clavicle")
+ end):SetIcon("icon16/compress.png")
+ collapses:AddOption("collapse legs", function()
+ local group = pac.CreatePart("group") group:SetParent(obj.Parent)
+ local right = obj
+ right:SetParent(group) right:SetSize(0) right:SetScaleChildren(true) right:SetBone("right thigh")
+ local left = pac.CreatePart("bone3") left:SetParent(group) left:SetSize(0) left:SetScaleChildren(true) left:SetBone("left thigh")
+ end):SetIcon("icon16/compress.png")
+ collapses:AddOption("collapse by keyword", function()
+ Derma_StringRequest("collapse bones", "please input a keyword to match", "head", function(str)
+ local group = pac.CreatePart("group") group:SetParent(obj.Parent)
+ local ent = obj:GetOwner()
+ for bone,tbl in pairs(pac.GetAllBones(ent)) do
+ if string.find(bone, str) ~= nil then
+ local newbone = pac.CreatePart("bone3") newbone:SetParent(group) newbone:SetSize(0) newbone:SetScaleChildren(true) newbone:SetBone(bone)
+ end
+ end
+ end)
+ end):SetIcon("icon16/text_align_center.png")
+ elseif obj.ClassName == "health_modifier" then
+ main:AddOption("setup HUD display for extra health (total)", function()
+ local cmd_on = pac.CreatePart("command") cmd_on:SetParent(obj) cmd_on:SetUseLua(true) cmd_on:SetName("enable HUD") cmd_on:SetExecuteOnWear(true)
+ local cmd_off = pac.CreatePart("command") cmd_off:SetParent(obj) cmd_off:SetUseLua(true) cmd_off:SetName("disable HUD") cmd_off:SetExecuteOnWear(false)
+ cmd_on:SetString([[surface.CreateFont("HudNumbers_Bigger", {font = "HudNumbers", size = 75})
+surface.CreateFont("HudNumbersGlow_Bigger", {font = "HudNumbersGlow", size = 75, blursize = 4, scanlines = 2, antialias = true})
+local x = 50
+local y = ScrH() - 190
+local clr = Color(255,230,0)
+hook.Add("HUDPaint", "extrahealth_total", function()
+ draw.DrawText("PAC EX HP", "Trebuchet24", x, y + 20, clr)
+ draw.DrawText("subtitle", "Trebuchet18", x, y + 40, clr)
+ draw.DrawText(LocalPlayer().pac_healthbars_total, "HudNumbersGlow_Bigger", x + 100, y, clr)
+ draw.DrawText(LocalPlayer().pac_healthbars_total, "HudNumbers_Bigger", x + 100, y, clr)
+end)]])
+ cmd_off:SetString([[hook.Remove("HUDPaint", "extrahealth_total")]])
+ end):SetIcon("icon16/application_xp_terminal.png")
+
+ main:AddOption("setup HUD display for extra health (this part only)", function()
+ local function setup()
+ local cmd_on = pac.CreatePart("command") cmd_on:SetParent(obj) cmd_on:SetUseLua(true) cmd_on:SetName("enable HUD") cmd_on:SetExecuteOnWear(true)
+ local cmd_off = pac.CreatePart("command") cmd_off:SetParent(obj) cmd_off:SetUseLua(true) cmd_off:SetName("disable HUD") cmd_off:SetExecuteOnWear(false)
+ cmd_on:SetString([[surface.CreateFont("HudNumbers_Bigger", {font = "HudNumbers", size = 75})
+surface.CreateFont("HudNumbersGlow_Bigger", {font = "HudNumbersGlow", size = 75, blursize = 4, scanlines = 2, antialias = true})
+local x = 50
+local y = ScrH() - 190
+local clr = Color(255,230,0)
+hook.Add("HUDPaint", "extrahealth_]]..obj.UniqueID..[[", function()
+ draw.DrawText("PAC EX HP\n]]..obj:GetName()..[[", "Trebuchet24", x, y + 20, clr)
+ draw.DrawText(LocalPlayer().pac_healthbars_uidtotals["]]..obj.UniqueID..[["], "HudNumbersGlow_Bigger", x + 100, y, clr)
+ draw.DrawText(LocalPlayer().pac_healthbars_uidtotals["]]..obj.UniqueID..[["], "HudNumbers_Bigger", x + 100, y, clr)
+end)]])
+ cmd_off:SetString([[hook.Remove("HUDPaint", "extrahealth_]]..obj.UniqueID..[[")]])
+ end
+ if obj.Name == "" then
+ Derma_StringRequest("prompt", "Looks like your health modifier doesn't have a name.\ngive it one?", "", function(str) obj:SetName(str) setup() end)
+ else
+ setup(obj.Name)
+ end
+ end):SetIcon("icon16/application_xp_terminal.png")
+
+ main:AddOption("setup HUD display for extra health (this layer)", function()
+ local cmd_on = pac.CreatePart("command") cmd_on:SetParent(obj) cmd_on:SetUseLua(true) cmd_on:SetName("enable HUD") cmd_on:SetExecuteOnWear(true)
+ local cmd_off = pac.CreatePart("command") cmd_off:SetParent(obj) cmd_off:SetUseLua(true) cmd_off:SetName("disable HUD") cmd_off:SetExecuteOnWear(false)
+ cmd_on:SetString([[surface.CreateFont("HudNumbers_Bigger", {font = "HudNumbers", size = 75})
+surface.CreateFont("HudNumbersGlow_Bigger", {font = "HudNumbersGlow", size = 75, blursize = 4, scanlines = 2, antialias = true})
+local x = 50
+local y = ScrH() - 190
+local clr = Color(255,230,0)
+hook.Add("HUDPaint", "extrahealth_layer_]]..obj.BarsLayer..[[", function()
+ draw.DrawText("PAC EX HP\nLYR]]..obj.BarsLayer..[[", "Trebuchet24", x, y + 20, clr)
+ draw.DrawText(LocalPlayer().pac_healthbars_layertotals[]]..obj.BarsLayer..[[], "HudNumbersGlow_Bigger", x + 100, y, clr)
+ draw.DrawText(LocalPlayer().pac_healthbars_layertotals[]]..obj.BarsLayer..[[], "HudNumbers_Bigger", x + 100, y, clr)
+end)]])
+ cmd_off:SetString([[hook.Remove("HUDPaint", "extrahealth_layer_]]..obj.BarsLayer..[[")]])
+ end):SetIcon("icon16/application_xp_terminal.png")
+
+ main:AddOption("Use extra health (total value) in a proxy", function() local proxy = pac.CreatePart("proxy") proxy:SetParent(obj) proxy:SetExpression("pac_healthbars_total()") proxy:SetExtra1(obj.Expression) end):SetIcon("icon16/calculator.png")
+ main:AddOption("Use extra health (this part's current HP) in a proxy", function() local proxy = pac.CreatePart("proxy") proxy:SetParent(obj) proxy:SetExpression("pac_healthbar_uidvalue(\""..obj.UniqueID.."\")") end):SetIcon("icon16/calculator.png")
+ main:AddOption("Use extra health (this part's remaining number of bars) in a proxy", function() local proxy = pac.CreatePart("proxy") proxy:SetParent(obj) proxy:SetExpression("pac_healthbar_remaining_bars(\""..obj.UniqueID.."\")") end):SetIcon("icon16/calculator.png")
+ main:AddOption("Use extra health (this layer's current total value) in a proxy", function() local proxy = pac.CreatePart("proxy") proxy:SetParent(obj) proxy:SetExpression("pac_healthbars_layertotal("..obj.BarsLayer..")") end):SetIcon("icon16/calculator.png")
+ elseif obj.ClassName == "hitscan" then
+ main:AddOption("approximate tracers from particles", function()
+ if not obj.previous_tracerparticle then
+ obj.previous_tracerparticle = pac.CreatePart("particles")
+ end
+ local particle = obj.previous_tracerparticle
+ particle:SetParent(obj)
+ particle:SetNumberParticles(obj.NumberBullets) particle:SetDieTime(0.3)
+ particle:SetSpread(obj.Spread) obj:SetTracerSparseness(0)
+ particle:SetMaterial("sprites/orangecore1") particle:SetLighting(false) particle:SetCollide(false)
+ particle:SetFireOnce(true) particle:SetStartSize(10) particle:SetEndSize(0) particle:SetStartLength(250) particle:SetEndLength(2000)
+ particle:SetGravity(Vector(0,0,0))
+ end):SetIcon("icon16/water.png")
+ elseif obj.ClassName == "jiggle" then
+ main:AddOption("Limit Angles", function()
+ obj:SetClampAngles(true) obj:SetAngleClampAmount(Vector(50,50,50))
+ end):SetIcon("icon16/compress.png")
+ local named_part = obj.Parent or obj
+ if not IsValid(named_part) then named_part = obj end
+ if pace.recently_substituted_movable_part then
+ if pace.recently_substituted_movable_part.Parent == obj then
+ named_part = pace.recently_substituted_movable_part
+ end
+ end
+ local str = named_part:GetName() str = string.Replace(str," ","")
+ main:AddOption("jiggle speed trick: deployable anchor (hidden by event)", function()
+ obj:SetSpeed(0) obj:SetResetOnHide(true)
+ local event = pac.CreatePart("event") event:SetParent(obj)
+ event:SetEvent("command") event:SetArguments("jiggle_anchor_"..str)
+ end):SetIcon("icon16/anchor.png")
+ main:AddOption("jiggle speed trick: movable anchor (proxy control)", function()
+ obj:SetSpeed(0) obj:SetResetOnHide(true)
+ local proxy = pac.CreatePart("proxy") proxy:SetParent(obj)
+ proxy:SetVariableName("Speed")
+ proxy:SetExpression("3") proxy:SetExpressionOnHide("0")
+ local event = pac.CreatePart("event") event:SetParent(proxy)
+ event:SetEvent("command") event:SetArguments("jiggle_anchor_"..str)
+ end):SetIcon("icon16/anchor.png")
+ elseif obj.ClassName == "interpolated_multibone" then
+ main:AddOption("rough demo: create random nodes", function()
+ local group = pac.CreatePart("group")
+ group:SetParent(obj.Parent)
+ obj:SetParent(group)
+ local axismodel = pac.CreatePart("model2") axismodel:SetParent(obj) newnode:SetModel("models/editor/axis_helper_thick.mdl") newnode:SetSize(5)
+ for i=1,5,1 do
+ local newnode = pac.CreatePart("model2") newnode:SetParent(obj.Parent) newnode:SetModel("models/empty.mdl")
+ newnode:SetName("test_node_"..i)
+ obj["SetNode"..i](obj,newnode)
+ newnode:SetPosition(VectorRand()*100) newnode:SetAngles(AngleRand()) newnode:SetBone(obj.Bone)
+ end
+ local proxy = pac.CreatePart("proxy")
+ proxy:SetParent(obj) proxy:SetVariableName("LerpValue") proxy:SetExpression("time()%6")
+ end):SetIcon("icon16/anchor.png")
+ main:AddOption("add node at camera (local head)", function()
+ if obj.Parent.ClassName == "group" and obj.Parent ~= obj:GetRootPart() then
+ obj.recent_parent = obj.Parent
+ end
+ if not obj.recent_parent then
+ local group = pac.CreatePart("group")
+ group:SetParent(obj.Parent)
+ obj:SetParent(group)
+ obj.recent_parent = group
+ end
+ local index = 1
+ for i=1,20,1 do
+ if not IsValid(obj["Node"..i]) then --free slot?
+ index = i
+ break
+ end
+ end
+ local newnode = pac.CreatePart("model2") newnode:SetParent(obj.Parent) newnode:SetModel("models/empty.mdl")
+ local localpos, localang = WorldToLocal(pace.ViewPos, pace.ViewAngles, newnode:GetWorldPosition(), newnode:GetWorldAngles())
+ newnode:SetNotes("recorded FOV : " .. math.Round(pace.ViewFOV))
+ newnode:SetName("cam_node_"..index)
+ obj["SetNode"..index](obj,newnode)
+ newnode:SetPosition(localpos) newnode:SetAngles(localang)
+ end):SetIcon("icon16/camera.png")
+ main:AddOption("add node at camera (entity invalidbone)", function()
+ local index = 1
+ for i=1,20,1 do
+ if not IsValid(obj["Node"..i]) then --free slot?
+ index = i
+ break
+ end
+ end
+ local newnode = pac.CreatePart("model2")
+ newnode:SetParent(obj:GetRootPart())
+
+ newnode:SetModel("models/empty.mdl")
+ newnode:SetBone("invalidbone")
+ local localpos, localang = WorldToLocal(pace.ViewPos, pace.ViewAngles, newnode:GetWorldPosition(), newnode:GetWorldAngles())
+ newnode:SetNotes("recorded FOV : " .. math.Round(pace.ViewFOV))
+ newnode:SetName("cam_node_"..index) newnode:SetBone("invalidbone")
+ obj["SetNode"..index](obj,newnode)
+ newnode:SetPosition(localpos) newnode:SetAngles(localang)
+ end):SetIcon("icon16/camera.png")
+ if #pace.BulkSelectList > 0 then
+ main:AddOption("(" .. #pace.BulkSelectList .. " parts in Bulk select) Set nodes (overwrite)", function()
+ for i=1,20,1 do
+ if pace.BulkSelectList[i] then
+ obj["SetNode"..i](obj,pace.BulkSelectList[i])
+ else
+ obj["SetNode"..i](obj,nil)
+ end
+ end
+ pace.PopulateProperties(obj)
+ end):SetIcon("icon16/pencil_delete.png")
+ main:AddOption("(" .. #pace.BulkSelectList .. " parts in Bulk select) Set nodes (append)", function()
+ for i=1,20,1 do
+ if not IsValid(obj["Node"..i]) then --free slot?
+ index = i
+ break
+ end
+ end
+ for i,part in ipairs(pace.BulkSelectList) do
+ obj["SetNode"..(index + i - 1)](obj,part)
+ end
+ pace.PopulateProperties(obj)
+ end):SetIcon("icon16/pencil_add.png")
+ end
+ end
+end
+
+--these are more to perform an action that doesn't really affect many different parameters. maybe one or two at most
+function pace.AddClassSpecificPartMenuComponents(menu, obj)
+ if obj.Notes == "showhidetest" then menu:AddOption("(hide/show test) reset", function() obj:CallRecursive("OnShow") end):SetIcon("icon16/star.png") end
+
+ if obj.ClassName == "camera" then
+ if not obj:IsHidden() then
+ local remembered_view = {pace.ViewPos, pace.ViewAngles}
+ local view
+ local viewing = obj == pac.active_camera
+ local initial_name = viewing and "Unview this camera" or "View this camera"
+ view = AddOptionRightClickable(initial_name, function()
+ if not viewing then
+ remembered_view = {pace.ViewPos, pace.ViewAngles}
+ pace.ManuallySelectCamera(obj, true)
+ view:SetText("Unview this camera")
+ else
+ pace.EnableView(true)
+ pace.ResetView()
+ pac.active_camera_manual = nil
+ if obj.pace_tree_node then
+ if obj.pace_tree_node.Icon then
+ if obj.pace_tree_node.Icon.event_icon then
+ obj.pace_tree_node.Icon.event_icon_alt = false
+ obj.pace_tree_node.Icon.event_icon:SetImage("event")
+ obj.pace_tree_node.Icon.event_icon:SetVisible(false)
+ end
+ end
+ end
+ pace.ViewPos = remembered_view[1]
+ pace.ViewAngles = remembered_view[2]
+ view:SetText("View this camera")
+ end
+ viewing = obj == pac.active_camera
+ end, menu) view:SetIcon("icon16/star.png")
+ else
+ menu:AddOption("View this camera", function()
+ local toggleable_command_events = {}
+ for part,reason in pairs(obj:GetReasonsHidden()) do
+ if reason == "event hiding" then
+ if part.Event == "command" then
+ local cmd, time, hide = part:GetParsedArgumentsForObject(part.Events.command)
+ if time == 0 then
+ toggleable_command_events[part] = cmd
+ end
+ end
+ end
+ end
+ for part,cmd in pairs(toggleable_command_events) do
+ RunConsoleCommand("pac_event", cmd, part.Invert and "1" or "0")
+ end
+ timer.Simple(0.1, function()
+ pace.ManuallySelectCamera(obj, true)
+ end)
+ end):SetIcon("icon16/star.png")
+ end
+ elseif obj.ClassName == "command" then
+ menu:AddOption("run command", function() obj:Execute() end):SetIcon("icon16/star.png")
+ elseif obj.ClassName == "sound" or obj.ClassName == "sound2" then
+ menu:AddOption("play sound", function() obj:PlaySound() end):SetIcon("icon16/star.png")
+ elseif obj.ClassName == "projectile" then
+ local pos, ang = obj:GetDrawPosition()
+ menu:AddOption("fire", function() obj:Shoot(pos, ang, obj.NumberProjectiles) end):SetIcon("icon16/star.png")
+ elseif obj.ClassName == "hitscan" then
+ menu:AddOption("fire", function() obj:Shoot() end):SetIcon("icon16/star.png")
+ elseif obj.ClassName == "damage_zone" then
+ menu:AddOption("do damage", function() obj:OnShow() end):SetIcon("icon16/star.png")
+ menu:AddOption("debug: clear hit markers", function() obj:ClearHitMarkers() end):SetIcon("icon16/star.png")
+ elseif obj.ClassName == "force" and not obj.Continuous then
+ menu:AddOption("(non-continuous only) force impulse", function() obj:OnShow() end):SetIcon("icon16/star.png")
+ elseif obj.ClassName == "particles" then
+ if obj.FireOnce then
+ menu:AddOption("(FireOnce only) spew", function() obj:OnShow() end):SetIcon("icon16/star.png")
+ end
+ elseif obj.ClassName == "proxy" then
+ if string.find(obj.Expression, "timeex") or string.find(obj.Expression, "ezfade") then
+ menu:AddOption("(timeex) reset clock", function() obj:OnHide() obj:OnShow() end):SetIcon("icon16/star.png")
+ end
+ if not IsValid(obj.TargetPart) and obj.MultipleTargetParts == "" then
+ menu:AddOption("engrave / quick-link to parent", function()
+ if not obj.AffectChildrenOnly then
+ obj:SetTargetPart(obj:GetParent())
+ elseif #obj:GetChildrenList() == 1 then
+ obj:SetTargetPart(obj:GetChildrenList()[1])
+ end
+
+ end):SetIcon("icon16/star.png")
+ end
+ if #pace.BulkSelectList > 0 then
+ menu:AddOption("(" .. #pace.BulkSelectList .. " parts in Bulk select) Set multiple target parts", function()
+ local uid_tbl = {}
+ for i,part in ipairs(pace.BulkSelectList) do
+ table.insert(uid_tbl, part.UniqueID)
+ end
+ obj:SetMultipleTargetParts(table.concat(uid_tbl,";"))
+ end):SetIcon("icon16/star.png")
+ if obj.MultipleTargetParts ~= "" then
+ menu:AddOption("(" .. #pace.BulkSelectList .. " parts in Bulk select) Add to multiple target parts", function()
+ local anti_duplicate = {}
+ local uid_tbl = string.Split(obj.MultipleTargetParts,";")
+
+ for i,uid in ipairs(uid_tbl) do
+ anti_duplicate[uid] = uid
+ end
+ for i,part in ipairs(pace.BulkSelectList) do
+ anti_duplicate[part.UniqueID] = part.UniqueID
+ end
+ uid_tbl = {}
+ for _,uid in pairs(anti_duplicate) do
+ table.insert(uid_tbl, uid)
+ end
+ obj:SetMultipleTargetParts(table.concat(uid_tbl,";"))
+ end):SetIcon("icon16/star.png")
+ end
+ end
+ elseif obj.ClassName == "beam" then
+ if not IsValid(obj.TargetPart) and obj.MultipleEndPoints == "" then
+ menu:AddOption("Link parent as end point", function()
+ obj:SetEndPoint(obj:GetParent())
+ end):SetIcon("icon16/star.png")
+ end
+ if #pace.BulkSelectList > 0 then
+ menu:AddOption("(" .. #pace.BulkSelectList .. " parts in Bulk select) Set multiple end points", function()
+ local uid_tbl = {}
+ for i,part in ipairs(pace.BulkSelectList) do
+ if not part.GetWorldPosition then erroring = true else table.insert(uid_tbl, part.UniqueID) end
+ end
+ if erroring then pac.InfoPopup("Some selected parts were invalid endpoints as they are not base_movables", {pac_part = false, obj_type = "cursor", panel_exp_height = 100}) end
+ obj:SetMultipleEndPoints(table.concat(uid_tbl,";"))
+ end):SetIcon("icon16/star.png")
+ end
+ elseif obj.ClassName == "shake" then
+ menu:AddOption("activate (editor camera should be off)", function() obj:OnHide() obj:OnShow() end):SetIcon("icon16/star.png")
+ elseif obj.ClassName == "event" then
+ if obj.Event == "command" and pac.LocalPlayer.pac_command_events then
+ local cmd, time, hide = obj:GetParsedArgumentsForObject(obj.Events.command)
+ if time == 0 then --toggling mode
+ pac.LocalPlayer.pac_command_events[cmd] = pac.LocalPlayer.pac_command_events[cmd] or {name = cmd, time = pac.RealTime, on = 0}
+ ----MORE PAC JANK?? SOMETIMES, THE 2 NOTATION DOESN'T CHANGE THE STATE YET
+ if pac.LocalPlayer.pac_command_events[cmd].on == 1 then
+ menu:AddOption("(command) toggle", function() RunConsoleCommand("pac_event", cmd, "0") end):SetIcon("icon16/star.png")
+ else
+ menu:AddOption("(command) toggle", function() RunConsoleCommand("pac_event", cmd, "1") end):SetIcon("icon16/star.png")
+ end
+
+ else
+ menu:AddOption("(command) trigger", function() RunConsoleCommand("pac_event", cmd) end):SetIcon("icon16/star.png")
+ end
+
+ end
+ if #pace.BulkSelectList > 0 then
+ menu:AddOption("(" .. #pace.BulkSelectList .. " parts in Bulk select) Set multiple target parts", function()
+ local uid_tbl = {}
+ for i,part in ipairs(pace.BulkSelectList) do
+ table.insert(uid_tbl, part.UniqueID)
+ end
+ obj:SetMultipleTargetParts(table.concat(uid_tbl,";"))
+ end):SetIcon("icon16/star.png")
+ if obj.Event == "and_gate" or obj.Event == "or_gate" then
+ menu:AddOption("(" .. #pace.BulkSelectList .. " parts in Bulk select) Set AND or OR gate arguments", function()
+ local uid_tbl = {}
+ for i,part in ipairs(pace.BulkSelectList) do
+ table.insert(uid_tbl, part.UniqueID)
+ end
+ obj:SetProperty("uids", table.concat(uid_tbl,";"))
+ end):SetIcon("icon16/clock_link.png")
+ end
+ if obj.Event == "xor_gate" and #pace.BulkSelectList == 2 then
+ menu:AddOption("(2 parts in Bulk select) Set XOR arguments", function()
+ local uid_tbl = {}
+ for i,part in ipairs(pace.BulkSelectList) do
+ table.insert(uid_tbl, part.UniqueID)
+ end
+ obj:SetArguments(table.concat(uid_tbl,"@@"))
+ end):SetIcon("icon16/clock_link.png")
+ end
+ end
+ if not IsValid(obj.DestinationPart) then
+ menu:AddOption("engrave / quick-link to parent", function() obj:SetDestinationPart(obj:GetParent()) end):SetIcon("icon16/star.png")
+ end
+ end
+
+ do --event reorganization
+ local full_events = true
+ for i,v in ipairs(pace.BulkSelectList) do
+ if v.ClassName ~= "event" then full_events = false end
+ end
+ if #pace.BulkSelectList > 0 and full_events then
+ menu:AddOption("reorganize into a non-ACO pocket", function()
+ for i,part in ipairs(pace.BulkSelectList) do
+ part:SetParent(part:GetRootPart())
+ end
+ local prime_parent = obj:GetParent()
+ if prime_parent.ClassName == "event" or pace.BulkSelectList[1] == prime_parent then
+ prime_parent = obj:GetRootPart()
+ end
+ for i,part in ipairs(pace.BulkSelectList) do
+ part:SetParent()
+ part:SetAffectChildrenOnly(false)
+ part:SetDestinationPart()
+ end
+ obj:SetParent(prime_parent)
+ for i,part in ipairs(pace.BulkSelectList) do
+ part:SetParent(obj)
+ end
+ end):SetIcon("icon16/clock_link.png")
+ menu:AddOption("reorganize into an ACO downward tower", function()
+ local parent = obj:GetParent()
+ local grandparent = obj:GetParent()
+ if parent.Parent then grandparent = parent:GetParent() end
+
+ for i,part in ipairs(pace.BulkSelectList) do
+ part:SetAffectChildrenOnly(true)
+ part:SetDestinationPart()
+ part:SetParent(parent)
+ parent = part
+ end
+ pace.BulkSelectList[1]:SetParent(obj:GetParent())
+ obj:SetParent(parent)
+ end):SetIcon("icon16/clock_link.png")
+ end
+ end
+
+ pace.AddQuickSetupsToPartMenu(menu, obj)
+end
+
+function pace.addPartMenuComponent(menu, obj, option_name)
+
+ if option_name == "save" and obj then
+ local save, pnl = menu:AddSubMenu(L"save", function() pace.SaveParts() end)
+ pnl:SetImage(pace.MiscIcons.save)
+ add_expensive_submenu_load(pnl, function() pace.AddSaveMenuToMenu(save, obj) end)
+ elseif option_name == "load" then
+ local load, pnl = menu:AddSubMenu(L"load", function() pace.LoadParts() end)
+ add_expensive_submenu_load(pnl, function() pace.AddSavedPartsToMenu(load, false, obj) end)
+ pnl:SetImage(pace.MiscIcons.load)
+ elseif option_name == "wear" and obj then
+ if not obj:HasParent() then
+ menu:AddOption(L"wear", function()
+ pace.SendPartToServer(obj)
+ pace.BulkSelectList = {}
+ end):SetImage(pace.MiscIcons.wear)
+ end
+ elseif option_name == "remove" and obj then
+ menu:AddOption(L"remove", function() pace.RemovePart(obj) end):SetImage(pace.MiscIcons.clear)
+ elseif option_name == "copy" and obj then
+ local menu2, pnl = menu:AddSubMenu(L"copy", function() pace.Copy(obj) end)
+ pnl:SetIcon(pace.MiscIcons.copy)
+ --menu:AddOption(L"copy", function() pace.Copy(obj) end):SetImage(pace.MiscIcons.copy)
+ menu2:AddOption(L"Copy part UniqueID", function() pace.CopyUID(obj) end):SetImage(pace.MiscIcons.uniqueid)
+ elseif option_name == "paste" and obj then
+ menu:AddOption(L"paste", function() pace.Paste(obj) end):SetImage(pace.MiscIcons.paste)
+ elseif option_name == "cut" and obj then
+ menu:AddOption(L"cut", function() pace.Cut(obj) end):SetImage("icon16/cut.png")
+ elseif option_name == "paste_properties" and obj then
+ menu:AddOption(L"paste properties", function() pace.PasteProperties(obj) end):SetImage(pace.MiscIcons.replace)
+ elseif option_name == "clone" and obj then
+ menu:AddOption(L"clone", function() pace.Clone(obj) end):SetImage(pace.MiscIcons.clone)
+ elseif option_name == "partsize_info" and obj then
+ local function GetTableSizeInfo(obj_arg)
+ return pace.GetPartSizeInformation(obj_arg)
+ end
+ local part_size_info, psi_icon = menu:AddSubMenu(L"get part size information", function()
+ local part_size_info = GetTableSizeInfo(obj)
+ local part_size_info_root = GetTableSizeInfo(obj:GetRootPart())
+
+ local part_size_info_root_processed = "\t" .. math.Round(100 * part_size_info.raw_bytes / part_size_info_root.raw_bytes,1) .. "% share of root "
+
+ local part_size_info_parent
+ local part_size_info_parent_processed
+ if IsValid(obj.Parent) then
+ part_size_info_parent = GetTableSizeInfo(obj.Parent)
+ part_size_info_parent_processed = "\t" .. math.Round(100 * part_size_info.raw_bytes / part_size_info_parent.raw_bytes,1) .. "% share of parent "
+ pac.Message(
+ obj, " " ..
+ part_size_info.info.."\n"..
+ part_size_info_parent_processed,obj.Parent,"\n"..
+ part_size_info_root_processed,obj:GetRootPart()
+ )
+ else
+ pac.Message(
+ obj, " " ..
+ part_size_info.info.."\n"..
+ part_size_info_root_processed,obj:GetRootPart()
+ )
+ end
+
+ end)
+ psi_icon:SetImage("icon16/drive.png")
+ part_size_info:AddOption(L"from bulk select", function()
+ local cumulative_bytes = 0
+ for _,v in pairs(pace.BulkSelectList) do
+ v.partsize_info = pace.GetPartSizeInformation(v)
+ cumulative_bytes = cumulative_bytes + 2*#util.TableToJSON(v:ToTable())
+ end
+
+ pac.Message("Bulk selected parts total " .. string.NiceSize(cumulative_bytes) .. "\nhere's the breakdown:")
+ for _,v in pairs(pace.BulkSelectList) do
+ local partsize_info = pace.GetPartSizeInformation(v)
+ MsgC(Color(100,255,100), string.NiceSize(partsize_info.raw_bytes)) MsgC(Color(200,200,200), " - ", v, "\n\t ")
+ MsgC(Color(0,255,255), math.Round(100 * partsize_info.raw_bytes/cumulative_bytes,1) .. "%")
+ MsgC(Color(200,200,200), " of bulk select total\n\t ")
+ MsgC(Color(0,255,255), math.Round(100 * partsize_info.raw_bytes/partsize_info.all_size_raw_bytes,1) .. "%")
+ MsgC(Color(200,200,200), " of total local parts)\n")
+ end
+ end)
+ elseif option_name == "arraying_menu" then
+ local arraying_menu, pnl = menu:AddSubMenu(L"arraying menu", function() pace.OpenArrayingMenu(obj) end) pnl:SetImage("icon16/table_multiple.png")
+ if obj.GetWorldPosition then
+ local icon = obj.pace_tree_node.ModelPath or obj.Icon
+ if string.sub(icon,-3) == "mdl" then icon = "materials/spawnicons/"..string.gsub(icon, ".mdl", "")..".png" end
+ arraying_menu:AddOption(L"base:" .. obj:GetName(), function() pace.OpenArrayingMenu(obj) end):SetImage(icon)
+ end
+ if obj.Parent.GetWorldPosition then
+ local icon = obj.pace_tree_node.ModelPath or obj.Icon
+ if string.sub(icon,-3) == "mdl" then icon = "materials/spawnicons/"..string.gsub(icon, ".mdl", "")..".png" end
+ arraying_menu:AddOption(L"base:" .. obj.Parent:GetName(), function() pace.OpenArrayingMenu(obj.Parent) end):SetImage(icon)
+ end
+ elseif option_name == "criteria_process" then
+ menu:AddOption("Process parts by criteria", function() pace.PromptProcessPartsByCriteria(obj) end):SetIcon("icon16/text_list_numbers.png")
+ elseif option_name == "bulk_morph" then
+ menu:AddOption("Morph Properties over bulk select", function() pace.BulkMorphProperty() end):SetIcon("icon16/chart_line.png")
+ elseif option_name == "bulk_apply_properties" then
+ local bulk_apply_properties,bap_icon = menu:AddSubMenu(L"bulk change properties", function() pace.BulkApplyProperties(obj, "harsh") end)
+ bap_icon:SetImage("icon16/application_form.png")
+ bulk_apply_properties:AddOption("Policy: harsh filtering", function() pace.BulkApplyProperties(obj, "harsh") end)
+ bulk_apply_properties:AddOption("Policy: lenient filtering", function() pace.BulkApplyProperties(obj, "lenient") end)
+ elseif option_name == "bulk_select" then
+ bulk_menu, bs_icon = menu:AddSubMenu(L"bulk select ("..#pace.BulkSelectList..")", function() pace.DoBulkSelect(obj) end)
+ bs_icon:SetImage("icon16/table_multiple.png")
+ bulk_menu.GetDeleteSelf = function() return false end
+
+ local mode = GetConVar("pac_bulk_select_halo_mode"):GetInt()
+ local info
+ if mode == 0 then info = "none"
+ elseif mode == 1 then info = "passive"
+ elseif mode == 2 then info = "custom keypress:"..GetConVar("pac_bulk_select_halo_key"):GetString()
+ elseif mode == 3 then info = "preset keypress: control"
+ elseif mode == 4 then info = "preset keypress: shift" end
+
+ bulk_menu:AddOption(L"Bulk select highlight mode: "..info, function()
+ Derma_StringRequest("Change bulk select halo highlighting mode", "0 is no highlighting\n1 is passive\n2 is when the same key as bulk select is pressed\n3 is when control key pressed\n4 is when shift key is pressed.",
+ tostring(mode), function(str) RunConsoleCommand("pac_bulk_select_halo_mode", str) end)
+ end):SetImage(pace.MiscIcons.info)
+ if #pace.BulkSelectList == 0 then
+ bulk_menu:AddOption(L"Bulk select info: nothing selected"):SetImage(pace.MiscIcons.info)
+ else
+ local copied, pnl = bulk_menu:AddSubMenu(L"Bulk select info: " .. #pace.BulkSelectList .. " copied parts")
+ pnl:SetImage(pace.MiscIcons.info)
+ for i,v in ipairs(pace.BulkSelectList) do
+ local name_str
+ if v.Name == "" then
+ name_str = tostring(v)
+ else
+ name_str = v.Name
+ end
+
+ copied:AddOption(i .. " : " .. name_str .. " (" .. v.ClassName .. ")"):SetIcon(v.Icon)
+ end
+ end
+ if #pace.BulkSelectClipboard == 0 then
+ bulk_menu:AddOption(L"Bulk select clipboard info: nothing copied"):SetImage(pace.MiscIcons.info)
+ else
+ local copied, pnl = bulk_menu:AddSubMenu(L"Bulk select clipboard info: " .. #pace.BulkSelectClipboard .. " copied parts")
+ pnl:SetImage(pace.MiscIcons.info)
+ for i,v in ipairs(pace.BulkSelectClipboard) do
+ local name_str
+ if v.Name == "" then
+ name_str = tostring(v)
+ else
+ name_str = v.Name
+ end
+
+ copied:AddOption(i .. " : " .. name_str .. " (" .. v.ClassName .. ")"):SetIcon(v.Icon)
+ end
+ end
+ local subsume_pnl = bulk_menu:AddCVar("bulk select subsume", "pac_bulk_select_subsume", "1", "0")
+ subsume_pnl:SetTooltip("Whether bulk select should take the hierarchy into account, deselecting children when selecting a part.\nEnable this if you commonly do broad operations like copying, deleting or moving parts.\nDisable this for targeted operations like property editing on nested model structures, for example.")
+ bulk_menu:AddCVar("draw bulk select info next to cursor", "pac_bulk_select_cursor_info", "1", "0")
+ local deselect_pnl = bulk_menu:AddCVar("bulk select deselect", "pac_bulk_select_deselect", "1", "0")
+ deselect_pnl:SetTooltip("Deselect all bulk selects if you select a part without holding bulk select key")
+
+ local resetting_mode, resetpnl = bulk_menu:AddSubMenu("Clear selection after operation?") resetpnl:SetImage("icon16/table_delete.png")
+ local resetting_mode1 = resetting_mode:AddOption("Yes") resetting_mode1:SetIsCheckable(true) resetting_mode1:SetRadio(true)
+ local resetting_mode2 = resetting_mode:AddOption("No") resetting_mode2:SetIsCheckable(true) resetting_mode2:SetRadio(true)
+ if pace.BulkSelect_clear_after_operation == nil then pace.BulkSelect_clear_after_operation = true end
+
+ function resetting_mode1.OnChecked(b)
+ pace.BulkSelect_clear_after_operation = true
+ end
+ function resetting_mode2.OnChecked(b)
+ pace.BulkSelect_clear_after_operation = false
+ end
+ if pace.BulkSelect_clear_after_operation then resetting_mode1:SetChecked(true) else resetting_mode2:SetChecked(true) end
+
+ bulk_menu:AddOption(L"Insert (Move / Cut + Paste)", function()
+ pace.BulkCutPaste(obj)
+ if pace.BulkSelect_clear_after_operation then pace.ClearBulkList() end
+ end):SetImage("icon16/arrow_join.png")
+
+ if not pace.ordered_operation_readystate then
+ bulk_menu:AddOption(L"prepare Ordered Insert (please select parts in order beforehand)", function()
+ pace.BulkCutPasteOrdered()
+ if pace.BulkSelect_clear_after_operation then pace.ClearBulkList() end
+ end):SetImage("icon16/text_list_numbers.png")
+ else
+ bulk_menu:AddOption(L"do Ordered Insert (select destinations in order)", function()
+ pace.BulkCutPasteOrdered()
+ if pace.BulkSelect_clear_after_operation then pace.ClearBulkList() end
+ end):SetImage("icon16/arrow_switch.png")
+ end
+
+
+ bulk_menu:AddOption(L"Copy to Bulk Clipboard", function()
+ pace.BulkCopy(obj)
+ if pace.BulkSelect_clear_after_operation then pace.ClearBulkList() end
+ end):SetImage(pace.MiscIcons.copy)
+
+ bulk_menu:AddSpacer()
+
+ --bulk paste modes
+ bulk_menu:AddOption(L"Bulk Paste (bulk select -> into this part)", function()
+ pace.BulkPasteFromBulkSelectToSinglePart(obj)
+ if pace.BulkSelect_clear_after_operation then pace.ClearBulkList() end
+ end):SetImage("icon16/arrow_join.png")
+
+ bulk_menu:AddOption(L"Bulk Paste (clipboard or this part -> into bulk selection)", function()
+ if not pace.Clipboard then pace.Copy(obj) end
+ pace.BulkPasteFromSingleClipboard()
+ if pace.BulkSelect_clear_after_operation then pace.ClearBulkList() end
+ end):SetImage("icon16/arrow_divide.png")
+
+ bulk_menu:AddOption(L"Bulk Paste (Single paste from bulk clipboard -> into this part)", function()
+ pace.BulkPasteFromBulkClipboard(obj)
+ if pace.BulkSelect_clear_after_operation then pace.ClearBulkList() end
+ end):SetImage("icon16/arrow_join.png")
+
+ bulk_menu:AddOption(L"Bulk Paste (Multi-paste from bulk clipboard -> into bulk selection)", function()
+ pace.BulkPasteFromBulkClipboardToBulkSelect()
+ if pace.BulkSelect_clear_after_operation then pace.ClearBulkList() end
+ end):SetImage("icon16/arrow_divide.png")
+
+ bulk_menu:AddSpacer()
+
+ bulk_menu:AddOption(L"Bulk paste properties from selected part", function()
+ pace.Copy(obj)
+ for _,v in ipairs(pace.BulkSelectList) do
+ pace.PasteProperties(v)
+ end
+ if pace.BulkSelect_clear_after_operation then pace.ClearBulkList() end
+ end):SetImage(pace.MiscIcons.replace)
+
+ bulk_menu:AddOption(L"Bulk paste properties from clipboard", function()
+ for _,v in ipairs(pace.BulkSelectList) do
+ pace.PasteProperties(v)
+ end
+ if pace.BulkSelect_clear_after_operation then pace.ClearBulkList() end
+ end):SetImage(pace.MiscIcons.replace)
+
+ bulk_menu:AddOption(L"Deploy a numbered command event series ("..#pace.BulkSelectList..")", function()
+ Derma_StringRequest(L"command series", L"input the base name", "", function(str)
+ str = string.gsub(str, " ", "")
+ for i,v in ipairs(pace.BulkSelectList) do
+ part = pac.CreatePart("event")
+ part:SetOperator("equal")
+ part:SetParent(v)
+ part.Event = "command"
+ part.Arguments = str..i.."@@0@@0"
+ end
+ if pace.BulkSelect_clear_after_operation then pace.ClearBulkList() end
+ end)
+ end):SetImage("icon16/clock.png")
+
+ bulk_menu:AddOption(L"Pack into a new group", function()
+ local root = pac.CreatePart("group")
+ root:SetParent(obj:GetParent())
+ for i,v in ipairs(pace.BulkSelectList) do
+ v:SetParent(root)
+ end
+ if pace.BulkSelect_clear_after_operation then pace.ClearBulkList() end
+ end):SetImage("icon16/world.png")
+ bulk_menu:AddOption(L"Pack into a new root group", function()
+ local root = pac.CreatePart("group")
+ for i,v in ipairs(pace.BulkSelectList) do
+ v:SetParent(root)
+ end
+ if pace.BulkSelect_clear_after_operation then pace.ClearBulkList() end
+ end):SetImage("icon16/world.png")
+
+ bulk_menu:AddSpacer()
+
+ bulk_menu:AddOption(L"Morph properties over bulk select", function()
+ pace.BulkMorphProperty()
+ end):SetImage("icon16/chart_line_edit.png")
+
+ bulk_menu:AddOption(L"bulk change properties", function() pace.BulkApplyProperties(obj, "harsh") end):SetImage("icon16/application_form.png")
+
+ local arraying_menu, pnl = bulk_menu:AddSubMenu(L"arraying menu", function() pace.OpenArrayingMenu(obj) end) pnl:SetImage("icon16/table_multiple.png")
+ if obj and obj.GetWorldPosition then
+ local icon = obj.pace_tree_node.ModelPath or obj.Icon
+ if string.sub(icon,-3) == "mdl" then icon = "materials/spawnicons/"..string.gsub(icon, ".mdl", "")..".png" end
+ arraying_menu:AddOption(L"base:" .. tostring(obj), function() pace.OpenArrayingMenu(obj) end):SetImage(icon)
+ end
+ if obj and obj.Parent.GetWorldPosition then
+ local icon = obj.pace_tree_node.ModelPath or obj.Icon
+ if string.sub(icon,-3) == "mdl" then icon = "materials/spawnicons/"..string.gsub(icon, ".mdl", "")..".png" end
+ arraying_menu:AddOption(L"base:" .. tostring(obj.Parent), function() pace.OpenArrayingMenu(obj.Parent) end):SetImage(icon)
+ end
+
+ bulk_menu:AddSpacer()
+
+ bulk_menu:AddOption(L"Bulk Delete", function()
+ pace.BulkRemovePart()
+ end):SetImage(pace.MiscIcons.clear)
+
+ bulk_menu:AddOption(L"Clear Bulk List", function()
+ pace.ClearBulkList()
+ end):SetImage("icon16/table_delete.png")
+ elseif option_name == "spacer" then
+ menu:AddSpacer()
+ elseif option_name == "registered_parts" then
+ pace.AddRegisteredPartsToMenu(menu, not obj)
+ elseif option_name == "hide_editor" then
+ menu:AddOption(L"hide editor / toggle focus", function() pace.Call("ToggleFocus") end):SetImage("icon16/zoom.png")
+ elseif option_name == "expand_all" and obj then
+ menu:AddOption(L"expand all", function()
+ obj:CallRecursive("SetEditorExpand", true)
+ pace.RefreshTree(true) end):SetImage("icon16/arrow_down.png")
+ elseif option_name == "collapse_all" and obj then
+ menu:AddOption(L"collapse all", function()
+ obj:CallRecursive("SetEditorExpand", false)
+ pace.RefreshTree(true) end):SetImage("icon16/arrow_in.png")
+ elseif option_name == "copy_uid" and obj then
+ local menu2, pnl = menu:AddSubMenu(L"Copy part UniqueID", function() pace.CopyUID(obj) end)
+ pnl:SetIcon(pace.MiscIcons.uniqueid)
+ elseif option_name == "help_part_info" and obj then
+ local pnl = menu:AddOption(L"View specific help or info about this part", function()
+ pac.AttachInfoPopupToPart(obj, nil, {
+ obj_type = GetConVar("pac_popups_preferred_location"):GetString(),
+ hoverfunc = "open",
+ pac_part = pace.current_part,
+ panel_exp_width = 900, panel_exp_height = 400
+ })
+ end) pnl:SetImage("icon16/information.png") pnl:SetTooltip("for some classes it'll be the same as hitting F1, giving you the basic class tutorial, but for proxies and events they will be more specific")
+ elseif option_name == "reorder_movables" and obj then
+ if (obj.Position and obj.Angles and obj.PositionOffset) then
+ local substitute, pnl = menu:AddSubMenu("Reorder / replace base movable")
+ pnl:SetImage("icon16/application_double.png")
+ substitute:AddOption("Create a parent for position substitution", function() pace.SubstituteBaseMovable(obj, "create_parent") end)
+ if obj.Parent then
+ if obj.Parent.Position and obj.Parent.Angles then
+ substitute:AddOption("Switch with parent", function() pace.SubstituteBaseMovable(obj, "reorder_child") end)
+ end
+ end
+ substitute:AddOption("Switch with another (select two parts with bulk select)", function() pace.SwapBaseMovables(pace.BulkSelectList[1], pace.BulkSelectList[2], false) end)
+ substitute:AddOption("Recast into new class (warning!)", function() pace.SubstituteBaseMovable(obj, "cast") end)
+ end
+ elseif option_name == "view_lockon" then
+ if not obj then return end
+ local function add_entity_version(obj, root_owner)
+ local root_owner = obj:GetRootPart():GetOwner()
+ local lockons, pnl2 = menu:AddSubMenu("lock on to " .. tostring(root_owner))
+ local function viewlock(mode)
+ if mode ~= "toggle" then
+ pace.viewlock_mode = mode
+ else
+ if pace.viewlock then
+ if pace.viewlock ~= root_owner then
+ pace.viewlock = root_owner
+ return
+ end
+ pace.viewlock = nil
+ return
+ end
+ pace.viewlock = root_owner
+ end
+ if mode == "disable" then
+ pace.ViewAngles.r = 0
+ pace.viewlock = nil
+ return
+ end
+ pace.viewlock_distance = pace.ViewPos:Distance(root_owner:GetPos() + root_owner:OBBCenter())
+ pace.viewlock = root_owner
+ end
+ lockons:AddOption("direct", function() viewlock("direct") end):SetImage("icon16/arrow_right.png")
+ lockons:AddOption("free pitch", function() viewlock("free pitch") end):SetImage("icon16/arrow_refresh.png")
+ lockons:AddOption("zero pitch", function() viewlock("zero pitch") end):SetImage("icon16/arrow_turn_right.png")
+ lockons:AddOption("disable", function() viewlock("disable") end):SetImage("icon16/cancel.png")
+ pnl2:SetImage("icon16/zoom.png")
+ end
+ local function add_part_version(obj)
+ local lockons, pnl2 = menu:AddSubMenu("lock on to " .. tostring(obj))
+ local function viewlock(mode)
+ if mode ~= "toggle" then
+ pace.viewlock_mode = mode
+ else
+ if pace.viewlock then
+ if pace.viewlock ~= obj then
+ pace.viewlock = obj
+ return
+ end
+ pace.viewlock = nil
+ return
+ end
+ pace.viewlock = obj
+ end
+ if mode == "disable" then
+ pace.ViewAngles.r = 0
+ pace.viewlock = nil
+ return
+ end
+ pace.viewlock_distance = pace.ViewPos:Distance(obj:GetWorldPosition())
+ pace.viewlock = obj
+ end
+ lockons:AddOption("direct", function() viewlock("direct") end):SetImage("icon16/arrow_right.png")
+ lockons:AddOption("free pitch", function() viewlock("free pitch") end):SetImage("icon16/arrow_refresh.png")
+ lockons:AddOption("zero pitch", function() viewlock("zero pitch") end):SetImage("icon16/arrow_turn_right.png")
+ lockons:AddOption("frame of reference (x)", function() pace.viewlock_axis = "x" viewlock("frame of reference") end):SetImage("icon16/arrow_branch.png")
+ lockons:AddOption("frame of reference (y)", function() pace.viewlock_axis = "y" viewlock("frame of reference") end):SetImage("icon16/arrow_branch.png")
+ lockons:AddOption("frame of reference (z)", function() pace.viewlock_axis = "z" viewlock("frame of reference") end):SetImage("icon16/arrow_branch.png")
+ lockons:AddOption("disable", function() viewlock("disable") end):SetImage("icon16/cancel.png")
+ pnl2:SetImage("icon16/zoom.png")
+ end
+ local is_root_entity = obj:GetOwner() == obj:GetRootPart():GetOwner()
+ if obj.ClassName == "group" then
+ if is_root_entity then
+ add_entity_version(obj, obj:GetRootPart():GetOwner())
+ elseif obj:GetOwner().GetWorldPosition then
+ add_part_version(obj:GetOwner())
+ end
+ elseif obj.GetWorldPosition then
+ add_part_version(obj)
+ end
+ elseif option_name == "view_goto" then
+ if not obj then return end
+ local is_root_entity = obj:GetOwner() == obj:GetRootPart():GetOwner()
+ if obj.ClassName == "group" then
+ if is_root_entity then
+ local gotos, pnl2 = menu:AddSubMenu("go to")
+ pnl2:SetImage("icon16/arrow_turn_right.png")
+ local axes = {"x","y","z","world_x","world_y","world_z"}
+ for _,ax in ipairs(axes) do
+ gotos:AddOption("+" .. ax, function()
+ pace.GoTo(obj:GetRootPart():GetOwner(), "view", {radius = 50, axis = ax})
+ end):SetImage("icon16/arrow_turn_right.png")
+ gotos:AddOption("-" .. ax, function()
+ pace.GoTo(obj:GetRootPart():GetOwner(), "view", {radius = -50, axis = ax})
+ end):SetImage("icon16/arrow_turn_right.png")
+ end
+ elseif obj:GetOwner().GetWorldPosition then
+ local gotos, pnl2 = menu:AddSubMenu("go to")
+ pnl2:SetImage("icon16/arrow_turn_right.png")
+ local axes = {"x","y","z","world_x","world_y","world_z"}
+ for _,ax in ipairs(axes) do
+ gotos:AddOption("+" .. ax, function()
+ pace.GoTo(obj, "view", {radius = 50, axis = ax})
+ end):SetImage("icon16/arrow_turn_right.png")
+ gotos:AddOption("-" .. ax, function()
+ pace.GoTo(obj, "view", {radius = -50, axis = ax})
+ end):SetImage("icon16/arrow_turn_right.png")
+ end
+ end
+ elseif obj.GetWorldPosition then
+ local gotos, pnl2 = menu:AddSubMenu("go to")
+ pnl2:SetImage("icon16/arrow_turn_right.png")
+ local axes = {"x","y","z","world_x","world_y","world_z"}
+ for _,ax in ipairs(axes) do
+ gotos:AddOption("+" .. ax, function()
+ pace.GoTo(obj, "view", {radius = 50, axis = ax})
+ end):SetImage("icon16/arrow_turn_right.png")
+ gotos:AddOption("-" .. ax, function()
+ pace.GoTo(obj, "view", {radius = -50, axis = ax})
+ end):SetImage("icon16/arrow_turn_right.png")
+ end
+ end
+
+ end
+
+end
+
+--destructive tool
+function pace.UltraCleanup(obj)
+ if not obj then return end
+
+ local root = obj
+ local safe_parts = {}
+ local parts_have_saved_parts = {}
+ local marked_for_deletion = {}
+
+ local function IsImportantMarked(part)
+ if not IsValid(part) then return false end
+ if part.Notes == "important" then return true end
+ return false
+ end
+
+ local function FoundImportantMarkedParent(part)
+ if not IsValid(part) then return false end
+ if IsImportantMarked(part) then return true end
+ local parent = part
+ while parent ~= root do
+ if parent.Notes and parent.Notes == "important" then return true end
+ parent = parent:GetParent()
+ end
+ return false
+ end
+
+ local function Important(part)
+ if not IsValid(part) then return false end
+ return IsImportantMarked(part) or FoundImportantMarkedParent(part)
+ end
+
+ local function CheckPartWithLinkedParts(part)
+ local found_parts = false
+ local part_roles = {
+ ["AimPart"] = nil, --base_movable
+ ["OutfitPart"] = nil, --projectile bullets
+ ["EndPoint"] = nil --beams
+ }
+
+ if part.ClassName == "projectile" then
+ if part.OutfitPart then
+ if part.OutfitPart:IsValid() then
+ part_roles["OutfitPart"] = part.OutfitPart
+ found_parts = true
+ end
+ end
+ end
+
+ if part.AimPart then
+ if part.AimPart:IsValid() then
+ part_roles["AimPart"] = part.AimPart
+ found_parts = true
+ end
+ end
+
+ if part.ClassName == "beam" then
+ if part.EndPoint then
+ if part.EndPoint:IsValid() then
+ part_roles["EndPoint"] = part.EndPoint
+ found_parts = true
+ end
+ end
+ end
+
+ parts_have_saved_parts[part] = found_parts
+ if found_parts then
+ safe_parts[part] = part
+ for i2,v2 in pairs(part_roles) do
+ if v2 then
+ safe_parts[v2] = v2
+ end
+ end
+ end
+ end
+
+ local function IsSafe(part)
+ local safe = true
+
+ if part.Notes then
+ if #(part.Notes) > 20 then return true end --assume if we write 20 characters in the notes then it's fine to keep it...
+ end
+
+ if part.ClassName == "event" or part.ClassName == "proxy" or part.ClassName == "command" then
+ return false
+ end
+
+ if not part:IsHidden() and not part.Hide then
+ else
+ safe = false
+ if string.find(part.ClassName,"material") then
+ safe = true
+ end
+ end
+
+ return safe
+ end
+
+ local function IsMildlyRisky(part)
+ if part.ClassName == "event" or part.ClassName == "proxy" or part.ClassName == "command" then
+ if not part:IsHidden() and not part.Hide then
+ return true
+ end
+ return false
+ end
+ return false
+ end
+
+ local function IsHangingPart(part)
+ if IsImportantMarked(part) then return false end
+ local c = part.ClassName
+
+ --unlooped sounds or 0 volume should be wiped
+ if c == "sound" or c == "ogg" or c == "webaudio" or c == "sound2" then
+ if part.Volume == 0 then return true end
+ if part.Loop ~= nil then
+ if not part.Loop then return true end
+ end
+ if part.PlayCount then
+ if part.PlayCount == 0 then return true end
+ end
+ end
+
+ --fireonce particles should be wiped
+ if c == "particle" then
+ if part.NumberParticles == 0 or part.FireOnce then return true end
+ end
+
+ --0 weight flexes have to be removed
+ if c == "flex" then
+ if part.Weight < 0.1 then return true end
+ end
+
+ if c == "sunbeams" then
+ if math.abs(part.Multiplier) == 0 then return true end
+ end
+
+ --other parts to leave forever
+ if c == "shake" or c == "gesture" then
+ return true
+ end
+ end
+
+ local function FindNearestSafeParent(part)
+ if not part then return end
+ local root = part:GetRootPart()
+ local parent = part:GetParent()
+ local child = part
+ local i = 0
+ while parent ~= root do
+ if i > 10 then return parent end
+ i = i + 1
+ if IsSafe(parent) then
+ return parent
+ elseif not IsMildlyRisky(parent) then
+ return parent
+ elseif not parent:IsHidden() and parent.Hide then
+ return parent
+ end
+ child = parent
+ parent = parent:GetParent()
+ end
+ return parent
+ end
+
+
+ local function SafeRemove(part)
+ if IsValid(part) then
+ if IsSafe(part) or Important(part) then
+ return
+ elseif IsMildlyRisky(part) then
+ if table.Count(part:GetChildren()) == 0 then
+ part:Remove()
+ end
+ end
+ end
+ end
+
+ --does algorithm needs to be recursive?
+ --delete absolute unsafes: hiddens.
+ --now there are safe events.
+ --extract children into nearest safe parent BUT ONLY DO IT VIA ITS DOMINANT and IF IT'S SAFE
+ --delete remaining unsafes: events, hiddens, commands ...
+ --but we still need to check for children to extract!
+
+ local function Move_contents_up(part) --this will be the powerhouse recursor
+ local parent = FindNearestSafeParent(part)
+ --print(part, "nearest parent is", parent)
+ for _,child in pairs(part:GetChildren()) do
+ if child:IsHidden() or child.Hide then --hidden = delete
+ marked_for_deletion[child] = child
+ else --visible = possible container = check
+ if table.Count(child:GetChildren()) == 0 then --dead end = immediate action
+ if IsSafe(child) then --safe = keep but now extract it
+ child:SetParent(parent)
+ --print(child, "moved to", parent)
+ safe_parts[child] = child
+ elseif child:IsHidden() or child.Hide then --hidden = delete
+ marked_for_deletion[child] = child
+ end
+ else --parent = process the children? done by the recursion
+ --the parent still needs to be moved up
+ child:SetParent(parent)
+
+ safe_parts[child] = child
+ Move_contents_up(child) --recurse
+ end
+
+ end
+
+ end
+ end
+
+ --find parts to delete
+ --first pass: absolute unsafes: hidden parts
+ for i,v in pairs(root:GetChildrenList()) do
+ if v:IsHidden() or v.Hide then
+ if not FoundImportantMarkedParent(v) then
+ v:Remove()
+ end
+
+ end
+ end
+
+ --second pass:
+ --A: mark safe parts
+ --B: extract children in remaining unsafes (i.e. break the chain of an event)
+ for i,v in pairs(root:GetChildrenList()) do
+ if IsSafe(v) then
+ safe_parts[v] = v
+ CheckPartWithLinkedParts(v)
+ if IsMildlyRisky(v:GetParent()) then
+ v:SetParent(v:GetParent():GetParent())
+ end
+ elseif IsMildlyRisky(v) then
+ Move_contents_up(v)
+ marked_for_deletion[v] = v
+ end
+
+ end
+ --after that, the remaining events etc are marked
+ for i,v in pairs(root:GetChildrenList()) do
+ if IsMildlyRisky(v) then
+ marked_for_deletion[v] = v
+ end
+ end
+
+ pace.RefreshTree()
+ --go through delete tables except when marked as important or those protected by these
+ for i,v in pairs(marked_for_deletion) do
+
+ local delete = false
+
+ if not safe_parts[v] then
+
+ if v:IsValid() then
+ delete = true
+ end
+ if FoundImportantMarkedParent(v) then
+ delete = false
+ end
+ end
+
+ if delete then SafeRemove(v) end
+ end
+
+ --third pass: cleanup the last remaining unwanted parts
+ for i,v in pairs(root:GetChildrenList()) do
+ --remove remaining events after their children have been freed, and delete parts that don't have durable use, like sounds that aren't looping
+ if IsMildlyRisky(v) or IsHangingPart(v) then
+ if not Important(v) then
+ v:Remove()
+ end
+ end
+ end
+
+ --fourth pass: delete bare containing nothing left
+ for i,v in pairs(root:GetChildrenList()) do
+ if v.ClassName == "group" then
+ local bare = true
+ for i2,v2 in pairs(v:GetChildrenList()) do
+ if v2.ClassName ~= "group" then
+ bare = false
+ end
+ end
+ if bare then v:Remove() end
+ end
+ end
+ pace.RefreshTree()
+
+end
+
+--match parts then replace properties or do other stuff like deleting
+function pace.ProcessPartsByCriteria(raw_args)
+
+ local match_criteria_tbl = {}
+ local process_actions = {}
+ local function match_criteria(part)
+ for i,v in ipairs(match_criteria_tbl) do
+ if v[2] == "=" then
+ if part[v[1]] ~= v[3] then
+ return false
+ end
+ elseif v[2] == ">" then
+ if part[v[1]] <= v[3] then
+ return false
+ end
+ elseif v[2] == ">=" then
+ if part[v[1]] < v[3] then
+ return false
+ end
+ elseif v[2] == "<" then
+ if part[v[1]] >= v[3] then
+ return false
+ end
+ elseif v[2] == "<=" then
+ if part[v[1]] > v[3] then
+ return false
+ end
+ else --bad operator
+ return false
+ end
+ end
+ return true
+ end
+ local function process(part)
+ print(part, "ready for processing")
+ for i,v in ipairs(process_actions) do
+ local action = v[1]
+ local key = v[2]
+ local value = v[3]
+ if action == "DELETE" then part:Remove() return end
+ if action == "REPLACE" then
+ if part["Set"..key] then
+ local type = type(part["Get"..key](part))
+ if type == "string" then
+ part["Set"..key](part,value)
+ elseif type == "Vector" then
+ local tbl = string.Split(value, " ")
+ if tbl[3] then
+ local vec = Vector(tonumber(tbl[1]),tonumber(tbl[2]),tonumber(tbl[3]))
+ part["Set"..key](part,vec)
+ end
+ elseif type == "Angle" then
+ local tbl = string.Split(value, " ")
+ if tbl[3] then
+ local ang = Angle(tonumber(tbl[1]),tonumber(tbl[2]),tonumber(tbl[3]))
+ part["Set"..key](part,ang)
+ end
+ elseif type == "number" then
+ part["Set"..key](part,tonumber(value))
+ elseif type == "boolean" then
+ part["Set"..key](part,tobool(value))
+ end
+ end
+ end
+ end
+ end
+
+ if isstring(raw_args) then
+ local reading_criteria = false
+ local reading_processing = false
+ local process
+ for i,line in ipairs(string.Split(raw_args, "\n")) do
+ local line_tbl = string.Split(line, "=")
+ if string.sub(line,1,8) == "CRITERIA" then
+ reading_criteria = true
+ elseif string.sub(line, 1,7) == "REPLACE" then
+ process = "REPLACE"
+ reading_criteria = false
+ reading_processing = true
+ elseif string.sub(line, 1,6) == "DELETE" then
+ process = "DELETE"
+ reading_criteria = false
+ reading_processing = true
+ elseif line ~= "" then
+ if reading_criteria then
+ table.insert(match_criteria_tbl, {line_tbl[1], "=", line_tbl[2]})
+ elseif reading_processing then
+ if process ~= nil then
+ table.insert(process_actions, {process, line_tbl[1], line_tbl[2] or ""})
+ end
+ end
+ end
+ end
+ elseif istable(raw_args) then
+ match_criteria_tbl = raw_args[1]
+ process_actions = raw_args[2]
+ else
+ return
+ end
+ pac.Message("PROCESS BY CRITERIA")
+ pac.Message("====================CRITERIA====================")
+ PrintTable(match_criteria_tbl)
+ print("\n")
+ pac.Message("====================PROCESSING====================")
+ PrintTable(process_actions)
+ pace.processing = true
+ for _,part in pairs(pac.GetLocalParts()) do
+ if match_criteria(part) then
+ process(part)
+ end
+ end
+ pace.processing = false
+end
+
+function pace.PromptProcessPartsByCriteria(part)
+ local default_args = ""
+ local default_class = ""
+ if part then
+ default_class = part.ClassName
+ if part.ClassName == "event" then
+ default_args = default_args .. "CRITERIA"
+ default_args = default_args .. "\nClassName=event"
+ default_args = default_args .. "\nArguments="..part:GetArguments()
+ default_args = default_args .. "\nEvent="..part:GetEvent()
+ default_args = default_args .. "\n\nREPLACE"
+ default_args = default_args .. "\nEvent="
+ default_args = default_args .. "\nArguments="
+ else
+ default_args = default_args .. "CRITERIA"
+ default_args = default_args .. "\nClassName=" .. default_class
+ default_args = default_args .. "\nKey=Value"
+ default_args = default_args .. "\n\nREPLACE"
+ default_args = default_args .. "\nKey=NewValue"
+ end
+ else
+ default_args = default_args .. "CRITERIA"
+ default_args = default_args .. "\nClassName=class"
+ default_args = default_args .. "\nKey=Value"
+ default_args = default_args .. "\n\nREPLACE"
+ default_args = default_args .. "\nKey=NewValue"
+ end
+ pace.MultilineStringRequest("Process by criteria", "enter arguments", default_args, function(str) pace.ProcessPartsByCriteria(str) end)
+end
+
+do --hover highlight halo
+ pac.haloex = include("pac3/libraries/haloex.lua")
+
+ local hover_halo_limit
+ local warn = false
+ local post_warn_next_allowed_check_time = 0
+ local post_warn_next_allowed_warn_time = 0
+ local last_culprit_UID = 0
+ local last_checked_partUID = 0
+ local last_tbl = {}
+ local last_bulk_select_tbl = nil
+ local last_root_ent = {}
+ local last_time_checked = 0
+
+ function pace.OnHoverPart(self)
+ local skip = false
+ if GetConVar("pac_hover_color"):GetString() == "none" then return end
+ hover_halo_limit = GetConVar("pac_hover_halo_limit"):GetInt()
+
+ local tbl = {}
+ local ent = self:GetOwner()
+ local is_root = ent == self:GetRootPart():GetOwner()
+
+ --decide whether to skip
+ --it will skip the part-search loop if we already checked the part recently
+ if self.UniqueID == last_checked_partUID then
+ skip = true
+ if is_root and last_root_ent ~= self:GetRootPart():GetOwner() then
+ table.RemoveByValue(last_tbl, last_root_ent)
+ table.insert(last_tbl, self:GetRootPart():GetOwner())
+ end
+ tbl = last_tbl
+ end
+
+ --operations : search the part and look for entity-candidates to halo
+ if not skip then
+ --start with entity, which could be part or entity
+ if (is_root and ent:IsValid()) then
+ table.insert(tbl, ent)
+ else
+ if not ((self.ClassName == "group" or self.ClassName == "jiggle") or (self.Hide == true) or (self.Size == 0) or (self.Alpha == 0)) then
+ table.insert(tbl, ent)
+ end
+ end
+
+ --get the children if any
+ if self:HasChildren() then
+ for _,v in ipairs(self:GetChildrenList()) do
+ local can_add = false
+ local ent = v:GetOwner()
+
+ --we're not gonna add parts that don't have a specific reason to be haloed or that don't at least group up some haloable models
+ --because the table.insert function has a processing load on the memory, and so is halo-drawing
+ if (v.ClassName == "model" or v.ClassName == "model2" or v.ClassName == "jiggle") then
+ can_add = true
+ else can_add = false end
+ if (v.Hide == true) or (v.Size == 0) or (v.Alpha == 0) or (v:IsHidden()) then
+ can_add = false
+ end
+ if can_add then table.insert(tbl, ent) end
+ end
+ end
+ end
+
+ last_tbl = tbl
+ last_root_ent = self:GetRootPart():GetOwner()
+ last_checked_partUID = self.UniqueID
+
+ DrawHaloHighlight(tbl)
+
+ --also refresh the bulk-selected nodes' labels because pace.RefreshTree() resets their alphas, but I want to keep the fade because it indicates what's being bulk-selected
+ if not skip then timer.Simple(0.3, function() BulkSelectRefreshFadedNodes(self) end) end
+ end
+
+end
+
+pace.arraying = false
+local last_clone = nil
+local axis_choice = "x"
+local axis_choice_id = 1
+local mode_choice = "Circle"
+local subdivisions = 1
+local length = 50
+local height = 50
+local offset = 0
+local save_settings = false
+local angle_follow = false
+
+function pace.OpenArrayingMenu(obj)
+ local locked_matrix_part = obj or pace.current_part
+ if locked_matrix_part.GetWorldPosition == nil then pace.FlashNotification("Please select a movable part before using the arraying menu") return end
+
+ local pos, ang = pace.mctrl.GetWorldPosition()
+ local mctrl = pos:ToScreen()
+ mctrl.x = mctrl.x + 100
+
+ local main_panel = vgui.Create("DFrame") main_panel:SetSize(600,400) main_panel:SetPos(mctrl.x + 100, mctrl.y - 200) main_panel:SetSizable(true)
+
+ main_panel:SetTitle("Arraying Menu - Please select an arrayed part contained inside " .. tostring(locked_matrix_part))
+ local properties_pnl = pace.CreatePanel("properties", main_panel) properties_pnl:SetSize(580,360) properties_pnl:SetPos(10,30)
+
+ properties_pnl:AddCollapser("Parts")
+ local matrix_part_selector = pace.CreatePanel("properties_part")
+ matrix_part_selector.part = locked_matrix_part
+ properties_pnl:AddKeyValue("Matrix",matrix_part_selector)
+ matrix_part_selector:SetValue(locked_matrix_part.UniqueID)
+ matrix_part_selector:PostInit()
+ local arraying_part_selector = pace.CreatePanel("properties_part")
+ properties_pnl:AddKeyValue("ArrayedPart",arraying_part_selector)
+ arraying_part_selector:PostInit()
+
+ properties_pnl:AddCollapser("Dimensions")
+ local height_slider = pace.CreatePanel("properties_number")
+ properties_pnl:AddKeyValue("Height",height_slider)
+ local length_slider = pace.CreatePanel("properties_number")
+ properties_pnl:AddKeyValue("Length",length_slider)
+ local array_modes = vgui.Create("DComboBox")
+ array_modes:AddChoice("Circle", "Circle", true, "icon16/cd.png")
+ array_modes:AddChoice("Rectangle", "Rectangle", false, "icon16/collision_on.png")
+ array_modes:AddChoice("Line", "Line", false, "icon16/chart_line.png")
+ properties_pnl:AddKeyValue("Mode",array_modes)
+ function array_modes:OnSelect(index, val, data) mode_choice = data end
+ local axes = vgui.Create("DComboBox")
+ axes:AddChoice("x", "x", true)
+ axes:AddChoice("y", "y", false)
+ axes:AddChoice("z", "z", false)
+ properties_pnl:AddKeyValue("Axis",axes)
+ function axes:OnSelect(index, val, data) axis_choice = data axis_choice_id = index end
+
+ properties_pnl:AddCollapser("Utilities")
+ local subdivs_slider = pace.CreatePanel("properties_number")
+ properties_pnl:AddKeyValue("Count",subdivs_slider)
+ local offset_slider = pace.CreatePanel("properties_number")
+ properties_pnl:AddKeyValue("Offset",offset_slider)
+ local anglefollow = pace.CreatePanel("properties_boolean")
+ properties_pnl:AddKeyValue("AlignToShape",anglefollow)
+ anglefollow:SetTooltip("Sets the Angles field in accordance to the shape. If you want to offset from that, use AngleOffset")
+ anglefollow:SetValue(false)
+ local savesettings = pace.CreatePanel("properties_boolean")
+ properties_pnl:AddKeyValue("SaveSettings",savesettings)
+ savesettings:SetTooltip("Preserves your settings if you close the window")
+ function savesettings.chck:OnChange(b) save_settings = b end
+ savesettings:SetValue(save_settings)
+ local force_update = vgui.Create("DButton")
+ force_update:SetText("Refresh")
+ force_update:SetTooltip("Updates clones (paste properties from the first part)")
+ properties_pnl:AddKeyValue("ForceUpdate",force_update)
+
+ if save_settings then
+ axes:ChooseOption(axis_choice, axis_choice_id)
+ anglefollow:SetValue(angle_follow)
+ subdivs_slider:SetValue(subdivisions)
+ offset_slider:SetValue(offset)
+ length_slider:SetValue(length)
+ height_slider:SetValue(height)
+ if last_clone then
+ arraying_part_selector:SetValue(last_clone.UniqueID)
+ arraying_part_selector:PostInit()
+ end
+ else
+ axes:ChooseOption("x",1)
+ anglefollow:SetValue(false)
+ subdivs_slider:SetValue(1)
+ offset_slider:SetValue(0)
+ length_slider:SetValue(50)
+ height_slider:SetValue(50)
+ end
+
+ local clone_positions = {}
+ local clones = {}
+ do
+ local toremove = {}
+ for i,v in ipairs(locked_matrix_part:GetChildren()) do
+ if v.Notes == "original array instance" then
+ last_clone = v
+ elseif v.Notes == "arrayed copy" then
+ table.insert(toremove, v)
+ end
+ end
+ if last_clone and save_settings then
+ arraying_part_selector:SetValue(last_clone.UniqueID)
+ end
+ for i,v in ipairs(toremove) do v:Remove() end
+ end
+
+ local clone_original = last_clone or arraying_part_selector.part
+
+ function main_panel:OnClose() pac.RemoveHook("PostDrawTranslucentRenderables", "ArrayingVisualize") pace.arraying = false end
+
+ local function get_basis(axis)
+
+ end
+
+ local function get_shape_angle(tbl, i)
+ if mode_choice == "Circle" then
+ return tbl.basis_angle * (tbl.index - 1) + tbl.offset_angle
+ elseif mode_choice == "Rectangle" then
+ return tbl.basis_angle
+ elseif mode_choice == "Line" then
+ if axis_choice == "x" then
+ if length >= 0 then return Angle(0,0,0) else return Angle(180,0,0) end
+ elseif axis_choice == "y" then
+ if length >= 0 then return tbl.basis_angle else return -tbl.basis_angle end
+ elseif axis_choice == "z" then
+ if length >= 0 then return tbl.basis_angle else return -tbl.basis_angle end
+ end
+ end
+ end
+
+ local function update_clones(recreate_parts)
+ for i,v in pairs(clones) do
+ if i > #clone_positions then
+ v:Remove()
+ clones[i] = nil
+ end
+ end
+
+ if arraying_part_selector:GetValue() == "" then print("empty boys") return end
+ clone_original = arraying_part_selector.part --pac.GetPartFromUniqueID(pac.Hash(LocalPlayer()), :DecodeEdit(arraying_part_selector:GetValue()))
+ last_clone = clone_original
+ local warning = false
+ if not clone_original then return end
+
+ if clone_original:HasChild(locked_matrix_part) or clone_original == locked_matrix_part then --avoid bad case of recursion
+ warning = true
+ end
+ for i,v in ipairs(clone_positions) do
+ if i ~= 1 then
+ local clone = clones[i]
+ if not clone then
+ --if recreate_parts and not warning then
+ clone = clone_original:Clone()
+ clone.Notes = "arrayed copy"
+ local name = "" .. i
+ if math.floor(math.log10(i)) == 0 then
+ name = "00" .. name
+ elseif math.floor(math.log10(i)) == 1 then
+ name = "0" .. name
+ end
+ clone.Name = "[" .. name .. "]"
+ clones[i] = clone
+ --end
+ end
+ clone:SetPosition(v.vec)
+ if anglefollow.chck:GetChecked() then
+ clone:SetAngleOffset(clone_original:GetAngleOffset())
+ clone:SetAngles(get_shape_angle(v, i-1))
+ --clone:SetAngles((i-1) * v.basis_angle + v.offset_angle)
+ end
+ else
+ if string.sub(clone_original:GetName(),1,5) ~= "[001]" then clone_original:SetName("[001]" .. clone_original:GetName()) end
+ clone_original:SetPosition(v.vec)
+ if anglefollow.chck:GetChecked() then
+
+ clone_original:SetAngles(get_shape_angle(v, i))
+ end
+ end
+ end
+ end
+
+ --that's a nice preview but what about local positions
+ local last_offset = 0
+ local function draw_circle(pos, basis_normal, basis_x, basis_y, length, height, subdivs)
+ --[[render.DrawLine(pos, pos + 50*basis_normal, Color(255,255,255), false)
+ render.DrawLine(pos, pos + 50*basis_x, Color(255,0,0), false)
+ render.DrawLine(pos, pos + 50*basis_y, Color(0,255,0), false)]]
+ clone_positions = {}
+ local radiansubdiv = 2*math.pi / subdivs
+ for i=0,subdivs,1 do
+ local pos1 = pos + math.sin(i*radiansubdiv)*basis_y*height + math.cos(i*radiansubdiv)*basis_x*length
+ local pos2 = pos + math.sin((i+1)*radiansubdiv)*basis_y*height + math.cos((i+1)*radiansubdiv)*basis_x*length
+ render.DrawLine(pos1,pos2,Color(255,255,200 + 50*math.sin(CurTime()*10)),true)
+ end
+ radiansubdiv = 2*math.pi / (subdivisions)
+ local matrix_pos, matrix_ang = locked_matrix_part:GetDrawPosition()
+ matrix_ang = matrix_ang -- locked_matrix_part.Angles
+ render.DrawLine(matrix_pos, matrix_pos + 50*matrix_ang:Forward(), Color(255,0,0), false)
+ render.DrawLine(matrix_pos, matrix_pos + 50*matrix_ang:Right(), Color(0,255,0), false)
+ for i=0,subdivisions,1 do
+ local degrees = offset + 360*i*radiansubdiv/(math.pi * 2)
+ local degrees2 = offset + 360*(i+1)*radiansubdiv/(math.pi * 2)
+ local radians = (degrees/360)*math.pi*2
+ local radians2 = (degrees2/360)*math.pi*2
+ if i == subdivisions then break end --don't make overlapping one
+ local ellipse_x = math.cos(radians)*length
+ local ellipse_y = math.sin(radians)*height
+ local pos1 = pos + math.sin(radians)*basis_y*height + math.cos(radians)*basis_x*length
+ local pos2 = pos + math.sin(radians2)*basis_y*height + math.cos(radians2)*basis_x*length
+
+ local the_original = false
+ if i == 0 then the_original = true end
+
+ local localpos, localang = WorldToLocal( pos1, ang, pos, matrix_ang )
+
+ local basis_angle = Angle()
+ local offset_angle = Angle()
+ if axis_choice == "y" then
+ basis_angle = Angle(-1,0,0) * (360 / subdivisions)
+ offset_angle = Angle(-offset,0,0)
+ elseif axis_choice == "z" then
+ basis_angle = Angle(0,-1,0) * (360 / subdivisions)
+ offset_angle = Angle(0,-offset,0)
+ elseif axis_choice == "x" then
+ basis_angle = Angle(0,0,1) * (360 / subdivisions)
+ offset_angle = Angle(0,0,offset)
+ end
+ table.insert(clone_positions, i+1, {wpos = pos1, wang = ang, vec = localpos, ang = localang, basis_angle = basis_angle, offset_angle = offset_angle, is_the_original = the_original, index = i+1, x = ellipse_x, y = ellipse_y, degrees = degrees})
+ end
+ if last_offset ~= offset_slider:GetValue() then last_offset = offset_slider:GetValue() update_clones() end
+ end
+ local function draw_rectangle(pos, basis_normal, basis_x, basis_y, length, height)
+ render.DrawLine(pos, pos + 50*basis_normal, Color(255,255,255), false)
+ render.DrawLine(pos, pos + 50*basis_x, Color(255,0,0), false)
+ render.DrawLine(pos, pos + 50*basis_y, Color(0,255,0), false)
+ clone_positions = {}
+
+ local x = basis_x*length
+ local y = basis_y*height
+ render.DrawLine(pos + x - y,pos + x + y,Color(255,255,200 + 50*math.sin(CurTime()*10)),true)
+ render.DrawLine(pos + x + y,pos - x + y,Color(255,255,200 + 50*math.sin(CurTime()*10)),true)
+ render.DrawLine(pos - x + y,pos - x - y,Color(255,255,200 + 50*math.sin(CurTime()*10)),true)
+ render.DrawLine(pos - x - y,pos + x - y,Color(255,255,200 + 50*math.sin(CurTime()*10)),true)
+
+ local matrix_pos, matrix_ang = locked_matrix_part:GetBonePosition()
+ for i=0,subdivisions,1 do
+ local frac = (offset/360 + (i-1)/subdivisions) % 1
+ local x
+ local y
+ local basis_ang_value = 0
+
+ if (frac >= 0.875) or (frac < 0.125) then --right side
+ x = basis_x*length
+ basis_ang_value = 0
+ if frac >= 0.875 then
+ y = 8*(frac-1)*basis_y*height
+ elseif frac < 0.125 then
+ y = 8*frac*basis_y*height
+ end
+ elseif (frac >= 0.125) and (frac < 0.375) then --up side
+ y = basis_y*height
+ basis_ang_value = 90
+ if frac < 0.25 then
+ x = 8*(-frac+0.25)*basis_x*length
+ elseif frac >= 0.25 then
+ x = 8*(-frac+0.25)*basis_x*length
+ end
+ elseif (frac >= 0.375) and (frac < 0.625) then --left side
+ x = -basis_x*length
+ basis_ang_value = 180
+ if frac < 0.5 then
+ y = 8*(-frac+0.5)*basis_y*height
+ elseif frac >= 0.5 then
+ y = 8*(-frac+0.5)*basis_y*height
+ end
+ elseif frac >= 0.625 then --down side
+ y = -basis_y*height
+ basis_ang_value = -90
+ if frac < 0.75 then
+ x = 8*(frac-0.75)*basis_x*length
+ elseif frac >= 0.75 then
+ x = 8*(frac-0.75)*basis_x*length
+ end
+ end
+ if i == subdivisions then break end --don't make overlapping one
+ local pos1 = pos + x + y
+
+ local the_original = false
+ if i == 0 then the_original = true end
+
+ local localpos, localang = WorldToLocal( pos1, matrix_ang, pos, ang )
+
+ local basis_angle = Angle()
+ local offset_angle = Angle()
+ if axis_choice == "x" then
+ basis_angle = Angle(0,0,1)*basis_ang_value
+ offset_angle = Angle(0,0,0)
+ elseif axis_choice == "y" then
+ basis_angle = Angle(-1,0,0)*basis_ang_value
+ offset_angle = Angle(0,0,0)
+ elseif axis_choice == "z" then
+ basis_angle = Angle(0,1,0)*basis_ang_value
+ offset_angle = Angle(0,0,0)
+ end
+ table.insert(clone_positions, i+1, {frac = frac, wpos = pos1, wang = ang, vec = localpos, ang = localang, basis_angle = basis_angle, offset_angle = offset_angle, is_the_original = the_original, index = i+1})
+
+ end
+ if last_offset ~= offset_slider:GetValue() then last_offset = offset_slider:GetValue() update_clones() end
+ end
+ local function draw_line(pos, basis_normal, length)
+ clone_positions = {}
+
+ render.DrawLine(pos, pos + basis_normal*length,Color(255,255,200 + 50*math.sin(CurTime()*10)),true)
+
+ for i=0,subdivisions,1 do
+ local forward = offset + (length*i)/subdivisions
+ local pos1 = pos + forward*basis_normal
+
+ local the_original = false
+ if i == 0 then the_original = true end
+
+ local localpos
+ local localang = Angle(0,0,0)
+ local basis_angle = Angle(0,0,0)
+ local offset_angle = Angle(0,0,0)
+ if axis_choice == "x" then
+ localpos = Vector(1,0,0)*forward
+ basis_angle = Angle(0,0,0)
+ elseif axis_choice == "y" then
+ localpos = Vector(0,1,0)*forward
+ basis_angle = Angle(0,90,0)
+ elseif axis_choice == "z" then
+ localpos = Vector(0,0,1)*forward
+ basis_angle = Angle(-90,0,0)
+ end
+
+
+ table.insert(clone_positions, i+1, {wpos = pos1, wang = ang, vec = localpos, ang = localang, basis_angle = basis_angle, offset_angle = offset_angle, is_the_original = the_original, index = i+1})
+ end
+ if last_offset ~= offset_slider:GetValue() then last_offset = offset_slider:GetValue() update_clones() end
+ end
+
+ --oof this one's gonna be rough how do we even do this
+ local function draw_clones()
+ update_clones(false)
+ for i,v in pairs(clone_positions) do
+ render.DrawLine(v.wpos, v.wpos + 10*v.wang:Forward(),Color(255,0,0),true)
+ render.DrawLine(v.wpos, v.wpos - 10*v.wang:Right(),Color(0,255,0),true)
+ render.DrawLine(v.wpos, v.wpos + 10*v.wang:Up(),Color(0,0,255),true)
+ if length < 10 or height < 10 then return end
+ if i == 1 then
+ render.SetMaterial(Material("sprites/grip_hover.vmt")) render.DrawSprite( v.wpos, 5, 5, Color( 255, 255, 255) )
+ else
+ render.SetMaterial(Material("sprites/grip.vmt")) render.DrawSprite( v.wpos, 3, 3, Color( 255, 255, 255) )
+ end
+ end
+ end
+
+ function subdivs_slider.OnValueChanged(val)
+ subdivisions = math.floor(val)
+ update_clones(true)
+ subdivs_slider:SetValue(math.floor(val))
+ end
+ function anglefollow.OnValueChanged(b)
+ angle_follow = b
+ anglefollow:SetValue(b)
+ end
+ function length_slider.OnValueChanged(val)
+ length = val
+ length_slider:SetValue(val)
+ end
+ function height_slider.OnValueChanged(val)
+ height = val
+ height_slider:SetValue(val)
+ end
+ function offset_slider.OnValueChanged(val)
+ offset = val
+ offset_slider:SetValue(val)
+ end
+ function force_update.DoClick()
+ local skip_properties = {
+ ["Position"] = true,
+ ["Angles"] = true,
+ ["Name"] = true,
+ ["Notes"] = true,
+ }
+ local originalpart = arraying_part_selector.part
+ local properties = originalpart:GetProperties()
+ for i,tbl in ipairs(properties) do
+ if skip_properties[tbl.key] then continue end
+ local val = originalpart["Get"..tbl.key](originalpart)
+ for _,part in pairs(clones) do
+ part["Set"..tbl.key](part, val)
+ end
+ end
+
+ end
+
+ if pace.arraying then pac.RemoveHook("PostDrawTranslucentRenderables", "ArrayingVisualize") pace.arraying = false return end
+
+ timer.Simple(0.3, function()
+ pac.AddHook("PostDrawTranslucentRenderables", "ArrayingVisualize", function()
+ matrix_part_selector.part = pac.GetPartFromUniqueID(pac.Hash(LocalPlayer()), matrix_part_selector:DecodeEdit(matrix_part_selector:GetValue()))
+ locked_matrix_part = matrix_part_selector.part
+ pace.mctrl.SetTarget(locked_matrix_part)
+ if arraying_part_selector:GetValue() then
+ if arraying_part_selector:GetValue() ~= locked_matrix_part.UniqueID then
+ arraying_part_selector.part = pac.GetPartFromUniqueID(pac.Hash(LocalPlayer()), arraying_part_selector:GetValue())
+ arraying_part_selector:SetValue(arraying_part_selector.part.UniqueID)
+ end
+ elseif pace.current_part ~= locked_matrix_part or ((arraying_part_selector.part ~= nil) and (arraying_part_selector.part ~= locked_matrix_part)) then
+ arraying_part_selector.part = pace.current_part
+ arraying_part_selector:SetValue(arraying_part_selector.part.UniqueID)
+ end
+
+ subdivisions = subdivs_slider:GetValue()
+ length = length_slider:GetValue()
+ height = height_slider:GetValue()
+ offset = offset_slider:GetValue()
+
+ --it's possible the part gets deleted
+ if not locked_matrix_part.GetDrawPosition then main_panel:Remove() pac.RemoveHook("PostDrawTranslucentRenderables", "ArrayingVisualize") return end
+ local pos, ang = locked_matrix_part:GetDrawPosition()
+ if not pos or not ang then return end
+ local forward, right, up = pace.mctrl.GetAxes(ang)
+
+ local basis_x, basis_y, basis_normal
+ if axis_choice == "x" then
+ basis_x = right
+ basis_y = up
+ basis_normal = forward
+ elseif axis_choice == "y" then
+ basis_x = forward
+ basis_y = up
+ basis_normal = right
+ elseif axis_choice == "z" then
+ basis_x = right
+ basis_y = forward
+ basis_normal = up
+ else
+ basis_x, basis_y, basis_normal = pace.mctrl.GetAxes(ang)
+ end
+
+ if not locked_matrix_part.GetWorldPosition then print("early exit 3") return end
+ if mode_choice == "Circle" then
+ draw_circle(pos, basis_normal, basis_x, basis_y, length_slider:GetValue(), height_slider:GetValue(), 40)
+ elseif mode_choice == "Rectangle" then
+ draw_rectangle(pos, basis_normal, basis_x, basis_y, length_slider:GetValue(), height_slider:GetValue())
+ elseif mode_choice == "Line" then
+ draw_line(pos, basis_normal, length_slider:GetValue())
+ end
+ draw_clones()
+ end)
+ end)
+
+ pace.arraying = true
+
+
+end
+
+--custom info panel
+--[[args
+tbl = {
+ obj = part.Label, --the associated object, could be a tree label, mouse, part etc.
+ pac_part = part --a pac part reference, if applicable
+ obj_type = "pac tree label",
+ hoverfunc = function() end,
+ doclickfunc = function() end,
+ panel_exp_width = 300, panel_exp_height = 200
+}
+
+]]
+
+local bsel_main_icon = Material("icon16/table_multiple.png")
+local bsel_clipboard_icon = Material("icon16/page_white_text.png")
+local white = Color(255,255,255)
+
+pac.AddHook("DrawOverlay", "bulkselect_cursor_info", function()
+ if not bulkselect_cursortext:GetBool() then return end
+ if not pace then return end
+ if not pace.IsFocused() then return end
+ local mx, my = input.GetCursorPos()
+
+ surface.SetFont("BudgetLabel")
+ surface.SetMaterial(Material("icon16/table_multiple.png"))
+ surface.SetTextColor(white)
+ surface.SetDrawColor(white)
+ local base_y = my + 8
+
+ if pace.BulkSelectList then
+ if #pace.BulkSelectList > 0 then
+ surface.DrawTexturedRect(mx + 10, base_y, 16, 16)
+ surface.SetTextPos(mx + 12 + 16, base_y)
+ surface.DrawText("bulk select [" .. #pace.BulkSelectList .."]")
+ base_y = base_y + 16
+ end
+ end
+ if pace.BulkSelectClipboard then
+ if #pace.BulkSelectClipboard > 0 then
+ surface.SetMaterial(Material("icon16/page_white_text.png"))
+ surface.DrawTexturedRect(mx + 10, base_y, 16, 16)
+ surface.SetTextPos(mx + 12 + 16, base_y)
+ surface.DrawText("bulk clipboard [" .. #pace.BulkSelectClipboard .."]")
+ end
+ end
+end)
\ No newline at end of file
diff --git a/lua/pac3/editor/client/popups_part_tutorials.lua b/lua/pac3/editor/client/popups_part_tutorials.lua
new file mode 100644
index 000000000..d49512460
--- /dev/null
+++ b/lua/pac3/editor/client/popups_part_tutorials.lua
@@ -0,0 +1,1321 @@
+--[[
+ This is the framework for popups. This should be expandable for various use cases.
+ It uses DFrame as a base, overrides the Paint function for a basic fade effect.
+
+ Tutorials will be written here
+]]
+
+local default_fonts = {
+ "BudgetLabel",
+ "CenterPrintText",
+ "ChatFont",
+ "CloseCaption_Bold",
+ "CloseCaption_BoldItalic",
+ "CloseCaption_Italic",
+ "CloseCaption_Normal",
+ "CreditsOutroText",
+ "CreditsText",
+ "DebugFixed",
+ "DebugFixedSmall",
+ "DebugOverlay",
+ "Default",
+ "DefaultFixed",
+ "DefaultFixedDropShadow",
+ "DefaultSmall",
+ "DefaultUnderline",
+ "DefaultVerySmall",
+ "HDRDemoText",
+ "HudDefault",
+ "HudHintTextLarge",
+ "HudHintTextSmall",
+ "HudSelectionText",
+ "TargetID",
+ "TargetIDSmall",
+ "Trebuchet18",
+ "Trebuchet24",
+ "DermaDefault",
+ "DermaDefaultBold",
+ "DermaLarge",
+ "GModNotify",
+ "ScoreboardDefault",
+ "ScoreboardDefaultTitle",
+
+}
+
+CreateConVar("pac_popups_enable", 1, FCVAR_ARCHIVE, "Enables PAC editor popups. They provide some information but can be annoying if you use autopilot options that make them automatically on certain contexts.")
+CreateConVar("pac_popups_font", "DermaDefaultBold", FCVAR_ARCHIVE, "PAC editor popups font")
+CreateConVar("pac_popups_preserve_on_autofade", 1, FCVAR_ARCHIVE, "If set to 0, PAC editor popups appear only once and don't reappear when hovering over the part label or pressing F1")
+CreateConVar("pac_popups_base_color", "255 255 255", FCVAR_ARCHIVE, "The color of the base filler rectangle for editor popups")
+CreateConVar("pac_popups_base_color_pulse", "0", FCVAR_ARCHIVE, "Amount of pulse of the base filler rectangle for editor popups")
+
+CreateConVar("pac_popups_base_alpha", "255", FCVAR_ARCHIVE, "The alpha opacity of the base filler rectangle for editor popups")
+CreateConVar("pac_popups_fade_color", "100 220 255", FCVAR_ARCHIVE, "The color of the fading effect for editor popups")
+CreateConVar("pac_popups_fade_alpha", "0", FCVAR_ARCHIVE, "The alpha opacity of the fading effect for editor popups")
+CreateConVar("pac_popups_text_color", "40 40 40", FCVAR_ARCHIVE, "The color of the fading effect for editor popups")
+CreateConVar("pac_popups_verbosity", "beginner tutorial", FCVAR_ARCHIVE, "Sets the amount of information added to PAC editor popups. While in development, there will be limited contextual support. If no special information is defined, it will indicate the part size information. Here are the planned modes: \nbeginner tutorial : Basic tutorials about pac parts, for beginners or casual users looking for a quick reference for what a part does\nReference tutorial : doesn't give part tutorials, but still keeps events' tutorial explanations.\n")
+CreateConVar("pac_popups_preferred_location", "pac tree label", FCVAR_ARCHIVE, "Sets the preferred method of PAC editor popups.\n"..
+ "pac tree label : the part label on the pac tree\n"..
+ "part world : if part is base_movable, place it next to the part in the viewport\n"..
+ "screen : static x,y on screen no matter what. That would be at the center\n"..
+ "cursor : right on the cursor\n"..
+ "menu bar : next to the toolbar")
+
+
+function pace.OpenPopupConfig()
+ local master_pnl = vgui.Create("DFrame")
+ master_pnl:SetTitle("Configure PAC3 popups appearance")
+ master_pnl:SetSize(400,800)
+ master_pnl:Center()
+
+ local list_pnl = vgui.Create("DListLayout", master_pnl)
+ list_pnl:Dock(FILL)
+
+ local basecolor = vgui.Create("DColorMixer")
+ basecolor:SetSize(400,150)
+ local col_args = string.Split(GetConVar("pac_popups_base_color"):GetString(), " ")
+ basecolor:SetColor(Color(col_args[1] or 255, col_args[2] or 255, col_args[3] or 255))
+ function basecolor:ValueChanged(col)
+ GetConVar("pac_popups_base_color"):SetString(col.r .. " " .. col.g .. " " .. col.b)
+ GetConVar("pac_popups_base_alpha"):SetString(col.a)
+ end
+ local basecolor_pulse = vgui.Create("DNumSlider")
+ basecolor_pulse:SetMax(255)
+ basecolor_pulse:SetMin(0)
+
+ if isnumber(GetConVar("pac_popups_base_color_pulse"):GetInt()) then
+ basecolor_pulse:SetValue(GetConVar("pac_popups_base_color_pulse"):GetInt())
+ else
+ basecolor_pulse:SetValue(0)
+ end
+
+ basecolor_pulse:SetText("base pulse")
+ function basecolor_pulse:OnValueChanged(val)
+ val = math.Round(tonumber(val),0)
+ GetConVar("pac_popups_base_color_pulse"):SetInt(val)
+ end
+
+ local fadecolor = vgui.Create("DColorMixer")
+ fadecolor:SetSize(400,150)
+ col_args = string.Split(GetConVar("pac_popups_fade_color"):GetString(), " ")
+ fadecolor:SetColor(Color(col_args[1] or 255, col_args[2] or 255, col_args[3] or 255))
+ function fadecolor:ValueChanged(col)
+ GetConVar("pac_popups_fade_color"):SetString(col.r .. " " .. col.g .. " " .. col.b)
+ GetConVar("pac_popups_fade_alpha"):SetString(col.a)
+ end
+
+ local textcolor = vgui.Create("DColorMixer")
+ textcolor:SetSize(400,150)
+ col_args = string.Split(GetConVar("pac_popups_text_color"):GetString(), " ")
+
+ if isnumber(col_args[1]) then
+ textcolor:SetColor(Color(col_args[1] or 255, col_args[2] or 255, col_args[3] or 255))
+ end
+
+ textcolor:SetAlphaBar(false)
+ function textcolor:ValueChanged(col)
+ GetConVar("pac_popups_text_color"):SetString(col.r .. " " .. col.g .. " " .. col.b)
+ end
+
+ pace.popups_font = GetConVar("pac_popups_font"):GetString()
+ local font = vgui.Create("DComboBox", master_pnl)
+ font:SetSize(200, 20)
+ font:SetPos(200,26)
+ font:SetText("font")
+ for _,f in ipairs(default_fonts) do
+ font:AddChoice(f,f)
+ end
+ function font:ChooseOption(val,id)
+ self:SetText(val)
+ GetConVar("pac_popups_font"):SetString(val)
+ pace.popups_font = val
+ end
+
+ local invertcolor_btn = vgui.Create("DButton")
+ invertcolor_btn:SetSize(400,30)
+ invertcolor_btn:SetText("Use text invert color (experimental)")
+ function invertcolor_btn:DoClick()
+ GetConVar("pac_popups_text_color"):SetString("invert")
+ end
+
+ local preview_pnl = vgui.Create("DLabel")
+ preview_pnl:SetSize(400,170)
+ preview_pnl:SetText("")
+ local label_text = "Popup preview! The text will look like this."
+
+ local rgb1 = string.Split(GetConVar("pac_popups_base_color"):GetString(), " ")
+ local r1,g1,b1 = tonumber(rgb1[1]) or 255, tonumber(rgb1[2]) or 255, tonumber(rgb1[3]) or 255
+ local a1 = GetConVar("pac_popups_base_alpha"):GetFloat()
+ local pulse = GetConVar("pac_popups_base_color_pulse"):GetInt()
+ local rgb2 = string.Split(GetConVar("pac_popups_fade_color"):GetString(), " ")
+ local r2,g2,b2 = tonumber(rgb2[1]) or 255, tonumber(rgb2[2]) or 255, tonumber(rgb2[3]) or 255
+ local a2 = GetConVar("pac_popups_fade_alpha"):GetFloat()
+ local rgb3 = string.Split(GetConVar("pac_popups_text_color"):GetString(), " ")
+ if rgb3[1] == "invert" then rgb3 = {nil,nil,nil} end
+ local r3,g3,b3 = tonumber(rgb3[1]) or (255 - (a1*r1/255 + a2*r2/255)/2), tonumber(rgb3[2]) or (255 - (a1*g1/255 + a2*g2/255)/2), tonumber(rgb3[3]) or (255 - (a1*b1/255 + a2*b2/255)/2)
+
+ local preview_refresh_btn = vgui.Create("DButton")
+ preview_refresh_btn:SetSize(400,30)
+ preview_refresh_btn:SetText("Refresh")
+ local oldpaintfunc = master_pnl.Paint
+ local invis_frame = false
+ function preview_refresh_btn:DoClick()
+ invis_frame = not invis_frame
+ if invis_frame then master_pnl.Paint = nil else master_pnl.Paint = oldpaintfunc end
+ rgb1 = string.Split(GetConVar("pac_popups_base_color"):GetString(), " ")
+ r1,g1,b1 = tonumber(rgb1[1]) or 255, tonumber(rgb1[2]) or 255, tonumber(rgb1[3]) or 255
+ a1 = GetConVar("pac_popups_base_alpha"):GetFloat()
+ pulse = GetConVar("pac_popups_base_color_pulse"):GetInt()
+ rgb2 = string.Split(GetConVar("pac_popups_fade_color"):GetString(), " ")
+ r2,g2,b2 = tonumber(rgb2[1]) or 255, tonumber(rgb2[2]) or 255, tonumber(rgb2[3]) or 255
+ a2 = GetConVar("pac_popups_fade_alpha"):GetFloat()
+ rgb3 = string.Split(GetConVar("pac_popups_text_color"):GetString(), " ")
+ if rgb3[1] == "invert" then rgb3 = {nil,nil,nil} end
+ r3,g3,b3 = tonumber(rgb3[1]) or (255 - (a1*r1/255 + a2*r2/255)/2), tonumber(rgb3[2]) or (255 - (a1*g1/255 + a2*g2/255)/2), tonumber(rgb3[3]) or (255 - (a1*b1/255 + a2*b2/255)/2)
+ end
+
+ function preview_pnl:Paint( w, h )
+ --base layer
+ local sine = 0.5 + 0.5*math.sin(CurTime()*2)
+ draw.RoundedBox( 0, 0, 0, w, h, Color( r1 - (r1/255)*pulse*sine, g1 - (g1/255)*pulse*sine, b1 - (b1/255)*pulse*sine, a1 - (a1/255)*pulse*sine) )
+ for band=0,w,1 do
+ --per-pixel fade
+ fade = 1 - (1/w * band * 1)
+ fade = math.pow(fade,2)
+ draw.RoundedBox( 0, band, 1, 1, h-2, Color( r2, g2, b2, fade*a2))
+ end
+ draw.DrawText(label_text, pace.popups_font, 5, 5, Color(r3,g3,b3,255))
+ end
+
+ list_pnl:Add(Label("Base color"))
+ list_pnl:Add(basecolor)
+ list_pnl:Add(basecolor_pulse)
+ list_pnl:Add(Label("Gradient color"))
+ list_pnl:Add(fadecolor)
+ list_pnl:Add(Label("Text color"))
+ list_pnl:Add(textcolor)
+ list_pnl:Add(invertcolor_btn)
+ list_pnl:Add(preview_refresh_btn)
+ list_pnl:Add(preview_pnl)
+ master_pnl:MakePopup()
+
+
+end
+
+concommand.Add("pac_popups_settings", function() pace.OpenPopupConfig() end)
+
+--[[
+ info_string, main string
+ { info about where to position the label
+ pac_part = part, that would be the pac part if applicable
+ obj = self.Label, that would be the positioning target
+ obj_type = "pac tree label", what type of thing is the target, for positioning
+ pac tree label = on the editor, needs to realign when scrolling
+ part world = if base_movable, place it next to the part in the view, if not, owner entity
+ screen = static x,y on screen no matter what, needs the further x,y args specified outside
+ cursor = right on the cursor
+ editor bar = next to the toolbar
+ hoverfunc = function() end, a function to run when hovering.
+ doclickfunc = function() end, a function to run when clicking
+ panel_exp_width = 900, panel_exp_height = 200 prescribed dimensions to expand to
+ },
+ self:LocalToScreen() x,y
+]]
+
+
+
+--[[
+we generally have two routes to create a popup: part and direct
+at the part level we can tell pac to try to create a popup
+pac.AttachInfoPopupToPart(part : obj, string : str, table : tbl) --naming scheme close to a general pac function
+ PART:AttachEditorPopup(string : str, bool : flash, table : tbl) --calls the generic base setup in base_part, shouldn't be overridden
+ PART:SetupEditorPopup(str, force_open, tbl) --calls the specific setup, can be overridden for different classes
+ pac.InfoPopup(str, tbl, x, y) --creates the vgui element
+
+we can directly create an independent editor popup
+pac.InfoPopup(str, tbl, x, y)
+]]
+
+
+function pac.InfoPopup(str, tbl, x, y)
+ if not GetConVar("pac_popups_enable"):GetBool() then return end
+ local x = x
+ local y = y
+ if not x or not y then
+ x = ScrW()/2 + math.Rand(-300,300)
+ y = ScrH()/2 + math.Rand(-300,0)
+ end
+ tbl = tbl or {}
+ if not tbl.obj then
+ if tbl.obj_type == "pac tree label" then
+ tbl.obj = tbl.pac_part.pace_tree_node
+ elseif tbl.obj_type == "part world" then
+ tbl.obj = tbl.pac_part
+ end
+ end
+
+ str = str or ""
+ local verbosity = GetConVar("pac_popups_verbosity"):GetString()
+
+ local rgb1 = string.Split(GetConVar("pac_popups_base_color"):GetString(), " ")
+ local r1,g1,b1 = tonumber(rgb1[1]) or 255, tonumber(rgb1[2]) or 255, tonumber(rgb1[3]) or 255
+ local a1 = GetConVar("pac_popups_base_alpha"):GetFloat()
+ local pulse = GetConVar("pac_popups_base_color_pulse"):GetInt()
+ local rgb2 = string.Split(GetConVar("pac_popups_fade_color"):GetString(), " ")
+ local r2,g2,b2 = tonumber(rgb2[1]) or 255, tonumber(rgb2[2]) or 255, tonumber(rgb2[3]) or 255
+ local a2 = GetConVar("pac_popups_fade_alpha"):GetFloat()
+ local rgb3 = string.Split(GetConVar("pac_popups_text_color"):GetString(), " ")
+ if rgb3[1] == "invert" then rgb3 = {nil,nil,nil} end
+ local r3,g3,b3 = tonumber(rgb3[1]) or (255 - (a1*r1/255 + a2*r2/255)/2), tonumber(rgb3[2]) or (255 - (a1*g1/255 + a2*g2/255)/2), tonumber(rgb3[3]) or (255 - (a1*b1/255 + a2*b2/255)/2)
+
+ local pnl = vgui.Create("DFrame")
+ local txt_zone = vgui.Create("RichText", pnl)
+
+ --function pnl:PerformLayout() end
+ pnl:SetTitle("") pnl:SetText("") pnl:ShowCloseButton( false )
+ txt_zone:SetPos(5,25)
+ txt_zone:SetContentAlignment( 7 ) --top left
+
+ if tbl.pac_part then
+ if verbosity == "reference tutorial" or verbosity == "beginner tutorial" then
+ if pace.TUTORIALS.PartInfos[tbl.pac_part.ClassName] then
+ str = str .. "\n\n====================================================================\n\nPart Class Tutorial for " .. tbl.pac_part.ClassName .. "\n" .. pace.TUTORIALS.PartInfos[tbl.pac_part.ClassName].popup_tutorial .. "\n"
+ end
+ end
+ end
+
+
+ pnl.hoverfunc = function() end
+ pnl.doclickfunc = function() end
+ pnl.titletext = "Click for more information! (or F1)"
+ pnl.alternativetitle = "Right click / Alt+P to kill popups. \"pac_popups_preserve_on_autofade\" is set to " .. GetConVar("pac_popups_preserve_on_autofade"):GetInt() .. ", " .. (GetConVar("pac_popups_preserve_on_autofade"):GetBool() and "If it fades away, the popup is allowed to reappear on hover or F1" or "If it fades away, the popup will not reappear")
+
+ --pnl:SetPos(ScrW()/2 + math.Rand(-100,100), ScrH()/2 + math.Rand(-100,100))
+
+ function pnl:FixPartReference(tbl)
+ if not tbl or table.IsEmpty(tbl) then self:Remove() end
+ if tbl.pac_part then tbl.obj = tbl.pac_part.pace_tree_node end
+
+ end
+
+ function pnl:MoveToObj(tbl)
+ --self:MakePopup()
+ if tbl.obj_type == "pac tree label" then
+ if not IsValid(tbl.obj) then
+ self:FixPartReference(tbl)
+ self:SetPos(x,y)
+ else
+ local x,y = tbl.obj:LocalToScreen()
+ x = pace.Editor:GetWide()
+ --print(pace.Editor:GetWide(), input.GetCursorPos())
+ self:SetPos(x,y)
+ end
+ if pace then
+ if pace.Editor then
+ if pace.Editor.IsLeft then
+ if not pace.Editor:IsLeft() then
+ self:SetPos(pace.Editor:GetX() - self:GetWide(),self:GetY())
+ else
+ self:SetPos(pace.Editor:GetX() + pace.Editor:GetWide(),self:GetY())
+ end
+ end
+ end
+ end
+
+ elseif tbl.obj_type == "part world" then
+ if tbl.pac_part then
+ local ent = tbl.pac_part:GetRootPart():GetOwner()
+ if not IsValid(ent) then ent = pac.LocalPlayer end
+ local global_position = pac.LocalPlayer:GetPos()
+ if ent.GetPos then global_position = (ent:GetPos() + ent:OBBCenter()*1.5) end
+ if tbl.pac_part.GetWorldPosition then
+ global_position = tbl.pac_part:GetWorldPosition() --if part is a base_movable, we'll get its position right away
+ elseif tbl.pac_part:GetParent().GetWorldPosition then
+ global_position = tbl.pac_part:GetParent():GetWorldPosition() --if part isn't but has a base_movable parent, get that
+ end
+ local scr_tbl = global_position:ToScreen()
+ self:SetPos(scr_tbl.x, scr_tbl.y)
+ end
+
+ elseif tbl.obj_type == "screen" then
+ self:SetPos(x,y)
+
+ --[[elseif tbl.obj_type == "cursor" then
+ self:SetPos(input.GetCursorPos())]]
+
+ elseif tbl.obj_type == "tracking cursor" then
+ self:SetPos(input.GetCursorPos())
+
+ elseif tbl.obj_type == "menu bar" then
+ if not pace.Editor:IsLeft() then
+ self:SetPos(pace.Editor:GetX() - self:GetWide(),self:GetY())
+ else
+ self:SetPos(pace.Editor:GetX() + pace.Editor:GetWide(),self:GetY())
+ end
+ end
+
+ end
+
+ if tbl.obj_type == "cursor" then
+ pnl:SetPos(input.GetCursorPos())
+ end
+
+ if tbl then
+ pnl.tbl = tbl
+ pnl:MoveToObj(tbl)
+ if tbl.hoverfunc then
+ if tbl.hoverfunc == "open" then
+ pnl.hoverfunc = function()
+ pnl.hovering = true
+ pnl:keep_alive(3)
+ if not pnl.hovering and not pnl.expand then
+ pnl.resizing = true
+ pnl.expand = true
+
+ pnl.ResizeStartTime = CurTime()
+ pnl.ResizeEndTime = CurTime() + 0.3
+ end
+ end
+ else
+ pnl.hoverfunc = tbl.hoverfunc
+ end
+ end
+ pnl.exp_height = tbl.panel_exp_height or 400
+ pnl.exp_width = tbl.panel_exp_width or 800
+ end
+
+ pnl.exp_height = pnl.exp_height or 400
+ pnl.exp_width = pnl.exp_width or 800
+ pnl:SetSize(200,20)
+
+ pnl.DeathTimeAdd = 0
+ if GetConVar("pac_popups_preserve_on_autofade"):GetBool() then
+ pnl.DeathTimeAdd = 240
+ end
+ pnl.DeathTime = CurTime() + 13
+ pnl.FadeTime = CurTime() + 10
+ pnl.FadeDuration = pnl.DeathTime - pnl.FadeTime
+ pnl.ResizeEndTime = 0
+ pnl.ResizeStartTime = 0
+ pnl.resizing = false
+
+ function pnl:keep_alive(extra_time)
+ pnl.DeathTime = math.max(pnl.DeathTime, CurTime() + extra_time + pnl.FadeDuration)
+ pnl.FadeTime = math.max(pnl.FadeTime, CurTime() + extra_time)
+ pnl:SetAlpha(255)
+ end
+
+ --the header needs a label to click on to open the popup
+ function pnl:DoClick()
+
+ if input.IsKeyDown(KEY_F1) or (self:IsHovered() and not txt_zone:IsHovered()) then
+ pnl.expand = not pnl.expand
+ pnl.ResizeStartTime = CurTime()
+ pnl.ResizeEndTime = CurTime() + 0.3
+ pnl.resizing = true
+ end
+
+ pnl:keep_alive(3)
+ pnl.doclickfunc()
+ end
+
+ --handle positioning, expanding and termination
+ function pnl:Think()
+ self:MoveToObj(tbl)
+ if input.IsButtonDown(KEY_P) and input.IsButtonDown(KEY_LALT) then --auto-kill if alt-p
+ if tbl.pac_part then tbl.pac_part.killpopup = true end
+ self:Remove()
+ end
+
+ if input.IsMouseDown(MOUSE_RIGHT) then
+ if self:IsHovered() and not txt_zone:IsHovered() then
+ self:Remove()
+ end
+ end
+
+ self.F1_doclick_possible_at = self.F1_doclick_possible_at or 0
+ self.mouse_doclick_possible_at = self.mouse_doclick_possible_at or 0
+
+ if input.IsButtonDown(KEY_F1) then --expand if press F1, but only after a delay
+ if self.F1_doclick_possible_at == 0 then
+ self.F1_doclick_possible_at = CurTime() + 0.3
+ end
+ if CurTime() > self.F1_doclick_possible_at then
+ self.F1_doclick_possible_at = 0
+ self:DoClick()
+ end
+ end
+ if input.IsMouseDown(MOUSE_LEFT) and self:IsHovered() or self:IsChildHovered() then --expand if press mouse left
+ if self.mouse_doclick_possible_at == 0 then
+ self.mouse_doclick_possible_at = CurTime() + 1
+ end
+ if CurTime() > self.mouse_doclick_possible_at then
+ self.mouse_doclick_possible_at = 0
+ self:DoClick()
+ end
+ end
+ if not input.IsMouseDown(MOUSE_LEFT) then
+ self.mouse_doclick_possible_at = CurTime()
+ end
+
+ if not IsValid(tbl.pac_part) and tbl.pac_part ~= false and tbl.pac_part ~= nil then self:Remove() end
+ self.exp_width = self.exp_width or 800
+ self.exp_height = self.exp_height or 500
+ --resizing code, initially the label should start small
+ if self.resizing then
+ local expand_frac_w = math.Clamp((self.ResizeEndTime - CurTime()) / 0.3,0,1)
+ local expand_frac_h = math.Clamp((self.ResizeEndTime - (CurTime() - 0.5)) / 0.5,0,1)
+ local width,height
+ if not self.expand then
+ width = 200 + (self.exp_width - 200)*(expand_frac_w)
+ height = 20 + (self.exp_height - 20)*(expand_frac_h)
+ if self.hovering and not self:IsHovered() then self.hovering = false end
+ else
+ width = 200 + (self.exp_width - 200)*(1 - expand_frac_h)
+ height = 20 + (self.exp_height - 20)*(1 - expand_frac_w)
+
+ end
+ self:SetSize(width,height)
+ txt_zone:SetSize(width-10,height-30)
+ end
+
+
+ self.fade_factor = math.Clamp((self.DeathTime - CurTime()) / self.FadeDuration,0,1)
+ self.fade_factor = math.pow(self.fade_factor, 3)
+
+ if CurTime() > self.DeathTime + self.DeathTimeAdd then
+ self:Remove()
+ end
+ if pace.Focused then self:SetAlpha(255*self.fade_factor) end
+ if self:IsHovered() then
+ self:keep_alive(1)
+ self.hoverfunc()
+ if input.IsMouseDown(MOUSE_RIGHT) then
+ if tbl.pac_part then
+ tbl.pac_part.killpopup = true
+ end
+ self:Remove()
+ end
+
+ end
+
+ if not pace.Focused then
+ self.has_focus = false
+ self:AlphaTo(0, 0.1, 0)
+ self:KillFocus()
+ self:SetMouseInputEnabled(false)
+ self:SetKeyBoardInputEnabled(false)
+ gui.EnableScreenClicker(false)
+ else
+ if not self.has_focus then
+ self:RequestFocus()
+ self:MakePopup()
+ self.has_focus = true
+ end
+ end
+
+ function pnl:OnRemove()
+ if not GetConVar("pac_popups_preserve_on_autofade"):GetBool() then
+ tbl.pac_part.killpopup = true
+ end
+ end
+ end
+
+ pnl.doclickfunc = tbl.doclickfunc or function() end
+
+
+
+ pnl.exp_height = tbl.panel_exp_height
+ pnl.exp_width = tbl.panel_exp_width
+
+ --cast the convars values
+ r1 = tonumber(r1)
+ g1 = tonumber(g1)
+ b1 = tonumber(b1)
+ a1 = tonumber(a1)
+ r2 = tonumber(r2)
+ g2 = tonumber(g2)
+ b2 = tonumber(b2)
+ a2 = tonumber(a2)
+ r3 = tonumber(r3)
+ g3 = tonumber(g3)
+ b3 = tonumber(b3)
+
+ local col = Color(r3,g3,b3,255)
+
+ function txt_zone:PerformLayout()
+ self:SetFontInternal(pace.popups_font or "DermaDefaultBold")
+ txt_zone:SetBGColor(0,0,0,0)
+ txt_zone:SetFGColor(col)
+ end
+
+ function txt_zone:Think()
+ if self:IsHovered() then
+ pnl:keep_alive(3)
+ end
+ end
+
+ txt_zone:SetText("")
+ txt_zone:AppendText(str)
+
+ txt_zone:SetVerticalScrollbarEnabled(true)
+
+ function pnl:Paint( w, h )
+
+
+ self.fade_factor = self.fade_factor or 1
+ --base layer
+ local sine = 0.5 + 0.5*math.sin(CurTime()*2)
+ draw.RoundedBox( 0, 0, 0, w, h, Color( r1 - (r1/255)*pulse*sine, g1 - (g1/255)*pulse*sine, b1 - (b1/255)*pulse*sine, a1 - (a1/255)*pulse*sine) )
+ --draw.RoundedBox( 0, 0, 0, 1, h, Color( 88, 179, 255, 255))
+ for band=0,w,1 do
+ --per-pixel fade
+ fade = 1 - (1/w * band * self.fade_factor)
+ fade2 = math.pow(fade,3)
+ fade = math.pow(fade,2)
+ --draw.RoundedBox( c, x, y, w, h, color )
+ draw.RoundedBox( 0, band, 1, 1, h-2, Color( r2, g2, b2, fade*a2))
+ --draw.RoundedBox( 0, band, 0, 1, 1, Color( 88, 179, 255, 255))
+ --draw.RoundedBox( 0, band, h-1, 1, 1, Color( 0, 0, 0, 255))
+ end
+
+ if self.expand then
+ draw.DrawText(self.alternativetitle, pace.popups_font, 5, 5, Color(r3,g3,b3,self.fade_factor * 255))
+ else
+ draw.DrawText(self.titletext, pace.popups_font, 5, 5, Color(r3,g3,b3,self.fade_factor * 255))
+ end
+ end
+
+ pnl:MakePopup()
+ return pnl
+end
+
+function pac.AttachInfoPopupToPart(obj, str, tbl)
+ if not obj then return end
+ obj:AttachEditorPopup(str, true, tbl)
+end
+
+function pace.FlushInfoPopups()
+ for _,part in pairs(pac.GetLocalParts()) do
+ local node = part.pace_tree_node
+ if not node or not node:IsValid() then continue end
+ if node.popupinfopnl then
+ node.popupinfopnl:Remove()
+ node.popupinfopnl = nil
+ end
+ end
+
+end
+
+--[[
+ part classes info
+
+ideally we should have:
+1-a tooltip form (7 words max)
+ e.g. projectile: throws missiles into the world
+2-a fuller form for the popups (4-5 sentences or more if needed)
+ e.g. projectile: the projectile part creates physical entities and launches them forward.\n
+ the entity has physics but it can be clientside (visual) or serverside (physical)\n
+ by selecting an outfit part, the entity can bear a PAC3 part or group to have a PAC3 outfit of its own\n
+ the entity can do damage but servers can restrict that.
+
+but then again we should probably look for better ways for the full-length explanations,
+ maybe grab some of them from the wiki or have a web browser for the wiki
+]]
+
+do
+
+ pace.TUTORIALS = pace.TUTORIALS or {}
+ pace.TUTORIALS.PartInfos = {
+
+ ["trail"] = {
+ tooltip = "leaves a trail behind",
+ popup_tutorial =
+ "the trail part creates beams along its path to make a trail\n"..
+ "nothing unique that I need to tell you, this part is mostly self-explanatory.\n"..
+ "you can set how it looks, how big it becomes etc."
+ },
+
+ ["trail2"] = {
+ tooltip = "leaves a trail behind",
+ popup_tutorial =
+ "the trail part creates beams along its path to make a trail\n"..
+ "nothing unique that I need to tell you, this part is mostly self-explanatory.\n"..
+ "you can set how it looks, how big it becomes etc."
+ },
+
+ ["sound"] = {
+ tooltip = "plays sounds",
+ popup_tutorial = "plays sounds in wav, mp3, ogg formats.\n"..
+ "for random sounds, paste each path separated by semicolons e.g. sound1.wav;sound3.wav;sound8.wav\n"..
+ "we have a special bracket notation for sound lists: sound[1,50].wav\n\n"..
+ "some of the parameters to know:\n"..
+ "sound level affects the falloff along with volume; a good starting point is 70 level, 0.6 volume\n"..
+ "overlapping means it doesn't get cut off if hidden\n"..
+ "sequential plays sounds in a list in order once you have the semicolon or bracket notation;\n"..
+ "\tthe steps is how much you progress by each activation. it can go one by one (1), every other sound (2+), stay there (0) or go back (negative values)"
+ },
+
+ ["sound2"] = {
+ tooltip = "plays web sounds",
+ popup_tutorial = "plays sounds in wav, mp3, ogg formats, with the option to download sound files from the internet\n"..
+ "people usually use dropbox, google drive, other cloud hosts or their own server host to store and distribute their files. each has its limitations.\n\n"..
+ "WARNING! Downloading and using these sounds is only possible in the chromium branch of garry's mod!\n\n"..
+ "to randomize sounds, we still have the same notations as legacy sound:\n"..
+ "\tsemicolon notation e.g. path1.wav;https://url1.wav;https://url2.wav\n"..
+ "\tbracket notation e.g. sound[1,50].wav\n\n"..
+ "some of the parameters to know, you'll already know some of them from legacy sound:\n"..
+ "-radius affects the falloff distance\n"..
+ "-overlapping means it doesn't get cut off if hidden\n"..
+ "-sequential plays sounds in a list in order"
+ },
+
+ ["ogg"] = {
+ tooltip = "plays ogg sounds (broken)",
+ popup_tutorial = "This part is not supported anymore. Do not bother. Use the new web sound."
+ },
+
+ ["webaudio"] = {
+ tooltip = "plays web sounds (legacy)",
+ popup_tutorial = "This part is not supported anymore. Do not bother. Use the new web sound."
+ },
+
+
+ ["halo"] = {
+ tooltip = "makes models glow",
+ popup_tutorial =
+ "This part creates a halo around a model entity.\n"..
+ "That could be your playermodel or a pac3 model, but for some reason it doesn't work on your player if you have an entity part.\n"..
+ "passes is the thickness of the halo, amount is the brightness, blur x and y spread out the shape"
+ },
+
+ ["bodygroup"] = {
+ tooltip = "changes body parts on supported models",
+ popup_tutorial =
+ "Bodygroups are a Source engine model feature which allows to easily show or hide different pieces of a model\n"..
+ "those are often used for accessories and styles. But it won't work unless your model has bodygroups.\n"..
+ "this part does exactly that. but you might do that directly with the model part or entity part"
+ },
+
+ ["holdtype"] = {
+ tooltip = "changes your animation set",
+ popup_tutorial =
+ "this part allows you to change animations played in individual movement slots, so you can mix and match from the available animations in your playermodel\n"..
+ "a holdtype is a set of animations for holding one kind of weapon, such as one-handed pistols vs two-handed revolvers, rifles, melee weapons etc.\n"..
+ "The option is also in the normal animation part, but this part goes in more detail in choosing different animations"
+ },
+
+ ["clip"] = {
+ tooltip = "cuts a model in a plane (legacy)",
+ popup_tutorial =
+ "This part cuts off one side of the model in rendering.\n"..
+ "It only cuts in a plane, with the forward red arrow as its normal. there are no other shapes."
+ },
+
+ ["clip2"] = {
+ tooltip = "cuts a model in a plane",
+ popup_tutorial =
+ "This part cuts off one side of the model in rendering.\n"..
+ "It only cuts in a plane, with the forward red arrow as its normal. there are no other shapes."
+ },
+
+ ["model"] = {
+ tooltip = "places a model (legacy)",
+ popup_tutorial = "The old model part still does the basic things you need a model to do"
+ },
+
+ ["model2"] = {
+ tooltip = "places a model",
+ popup_tutorial =
+ "The model part creates a clientside entity to draw a model locally.\n"..
+ "Being a base_movable, parts inside it will be physically arented to it.\n"..
+ "therefore, it can act as a regrouper, rail or anchoring point for your pac structuring needs, although you probably shouldn't abuse it.\n"..
+ "It can accept most modifiers and play animations, if present or referenced in the model.\n\n"..
+ "It can load MDL zips or OBJ files from a direct link to a server host or cloud provider, allowing you to use pretty much any model as long as it's the right format for Source. And on that subject, you would do well to install Crowbar, as well as Blender with Blender Source Tools, if you want to extract and edit models. Consult the valve developer community wiki for more information about QC; I view this as common knowledge rather than the purview of pac3 so you have to do some research."
+ },
+
+ ["material"] = {
+ tooltip = "defines a material (legacy)",
+ popup_tutorial =
+ "the old material still works as it says. it lets you define some VMT parameters for a material"
+ },
+
+ ["material_3d"] = {
+ tooltip = "defines a material for models",
+ popup_tutorial =
+ "This part creates a VMT material of the shader type VertexLitGeneric.\n"..
+ "If you have experience in Source engine things, you probably should know what some of these do, I won't expound fully but here's the essential summary anyway:\n\n"..
+ "\tbase texture is the base image. It's basically just color pixels.\n"..
+ "\tbump map / normal map is a relief that gives a texture on the surface. It uses a distinctly purple pixel format; it's not color but directional information\n"..
+ "\tdetail is a second base image added on top to modify the pixels. It's usually grayscale because we don't need to add color to give more grit to an image\n"..
+ "\tself illumination and emissive blend are glowing layers. emissive blend is more complex and needs three necessary components before it starts to work properly.\n"..
+ "\tenvironment map is a layer for pre-baked room reflection, by default env_cubemap tries to get the nearest cubemap but you can choose another texture, although cubemaps are a very specific format\n"..
+ "\tphong is a layer of dynamic light reflections\n\n"..
+ "If you want to edit a material, you can load its VMT with a right click on \"load vmt\", then select the right material override\n"..
+ "Reminder that transparent textures may need additive or some form of translucent setting on the model and on the material.\n\n"..
+ "For more information, search the Valve developer community site or elsewhere. Many material features are standard, and if you want to push this part to the limit, the extra research will be worth it."
+ },
+
+ ["material_2d"] = {
+ tooltip = "defines a material for sprites",
+ popup_tutorial =
+ "This part creates a VMT material of the shader type UnlitGeneric. This is used by particles and sprites.\n"..
+ "For transparent textures, use additive or vertex alpha/vertex color (for particles and decals). Some VTF or PNG textures have an alpha channel, but many just have a black background meant for additive rendering.\n\n"..
+ "For more information, search the Valve developer community site"
+ },
+
+ ["material_refract"] = {
+ tooltip = "defines a refracting material",
+ popup_tutorial =
+ "This part creates a VMT material of the shader type Refract. As with other material parts, you would find it useful to name the material to use that in multiple models' \"material\" fields\n"..
+ "In a way, it doesn't work by surface, but by silhouette. But the surface does determine how the refraction occurs. Setting a base texture creates a flat wall behind it that can distort in interesting ways but it'll replace the view behind.\n"..
+ "The normal section does most of the heavy lifting. This is where the image behind the material gets refracted according to the surface. You can blend between two normal maps in greater detail.\n"..
+ "Your model needs to be set to \"translucent\" rendering mode for this to work because the shader is in a multi-step rendering process.\n\n"..
+ "For more information, search the Valve developer community site"
+ },
+
+ ["material_eye refract"] = {
+ tooltip = "defines a refracting eye material",
+ popup_tutorial =
+ "This part creates a VMT material of the shader type EyeRefract.\n"..
+ "It's tricky to use because of how it involves projections and entity eye position, but you can more easily get something working on premade HL2 or other Source games' characters with QC eyes."
+ },
+
+ ["submaterial"] = {
+ tooltip = "applies a material on a submaterial zone",
+ popup_tutorial =
+ "Models can be comprised of multiple materials in different areas. This part can replace the material applied to one of these zones.\n"..
+ "Depending on how the model was made, it might correspond to what you want, or it might not.\n"..
+ "As usual, as with other model modifiers your expectations should always line up with the quality of the model you're using."
+ },
+
+ ["bone"] = {
+ tooltip = "changes a bone (legacy)",
+ popup_tutorial =
+ "The legacy bone part still does the basic things you need a bone part to do, but you should probably use the new bone part."
+ },
+
+ ["bone2"] = {
+ tooltip = "changes a bone (legacy)",
+ popup_tutorial =
+ "The legacy experimental bone part still does the basic things you need a bone part to do, but you should probably use the new bone part."
+ },
+
+ ["bone3"] = {
+ tooltip = "changes a bone",
+ popup_tutorial =
+ "This part modifies a model's bone. It can move relative to the parent bone, scale, and rotate.\n"..
+ "Follow part forces the bone to relocate to a base_movable part. Might have issues if you successively follow part multiple related bones. You could try to fix that by changing draw orders of the follow parts and bones."
+ },
+
+ ["player_config"] = {
+ tooltip = "sets your player entity's behaviour",
+ popup_tutorial =
+ "This part has access to some of your player's behavior, like whether you will play footsteps, the chat animation etc.\n"..
+ "Some of these may or may not work as intended..."
+ },
+
+ ["light"] = {
+ tooltip = "lights up the world (legacy)",
+ popup_tutorial =
+ "This legacy part still does the basic thing you want from a light, but the new light part is more fully-featured, for the most part.\n"..
+ "There is one thing it does that the new part doesn't, and that's styles."
+ },
+
+ ["light2"] = {
+ tooltip = "lights up models or the world",
+ popup_tutorial =
+ "This part creates a dynamic light that can illuminate models or the world independently.\n"..
+ "There are some options for the light's falloff shape (inner and outer angles).\n"..
+ "Its brightness works by magnitude and size, not multiplication. Which means you can still have light at 0 or lower brightness."
+ },
+
+ ["event"] = {
+ tooltip = "activates or deactivates other parts",
+ popup_tutorial =
+ "This part hides or shows connected parts when certain conditions are met. We won't describe them in this tutorial, you'll have to read them individually. The essential behaviour remains common accross events.\n\n"..
+ "Domain, in other words, which parts get affected:\n"..
+ "\t1-Default: The event will command its direct parent. Because parts can contain other parts, this includes the event itself, and parts beside the event too. While this is not usually a problem, you have to be aware of that.\n"..
+ "\t2-ACO: Affect Children Only. The event will command parts inside it, not beside, not above. This is the first step to isolate your setup and have clean logic in your pac.\n"..
+ "\t3-Targeted: The event gets wired to a part directly, including its children of course. This is accessed when you select a part in the \"targeted part\" field, which has an unfortunate name because there's still the old \"target part\" parameter\n\n"..
+ "Some events, like is_touching, can select an external \"target\" to use as a point to gather information.\n\n"..
+ "Operators:\n"..
+ "Operators are just how the event asks the question to determine when to activate or deactivate. Just read the event the same way as it asks the question: is my source equal to the value? can I find this text in my source?\n"..
+ "\tnumber-related operators: equal, above, below, equal or above, equal or below\n"..
+ "\tstring-related operators: equal, find, find simple\n"..
+ "There's still a caveat. If you use the wrong type of operator for your event, it will NOT work. Please trust the editor autopilot when it automatically changes your operator to a good one. Do not change it unless you know what you're doing."
+ },
+
+ ["sprite"] = {
+ tooltip = "draws a 2D texture",
+ popup_tutorial =
+ "Sprites are another Source engine thing which are useful for some point effects. Most textures being for model surfaces will look like squares if drawn flat, but sprite and particle textures are made specially for this purpose.\n"..
+ "They should have a transparent background or black background. The difference is because of rendering modes or blend modes.\n"..
+ "Additive rendering adds pixels' values. So, bright pixels will be more visible, but dark pixels end up being faded or invisible because their amounts are low."
+ },
+
+ ["fog"] = {
+ tooltip = "colors a model with fog",
+ popup_tutorial =
+ "This strange modifier renders a fog-like color over a model. Not in the world, not inside the model, but over its surface.\n"..
+ "For that reason, you might do well to change rendering-related values like blend mode on the host model's side\n"..
+ "It requires to be attached to a base_drawable part, keep in mind the start and end values are multiplied by 100 in post for some reason."..
+ "start is the distance where the fog starts to appear outside, end is where the fog is thickest."
+ },
+
+ ["force"] = {
+ tooltip = "provides physical force",
+ popup_tutorial =
+ "This part tries to tell the server to do a force impulse, or continually request small impulses for a continuous force. It should work for most physics props, some item and ammo entities, players and NPCs. But it may or may not be allowed on the server due to server settings: pac_sv_force.\n\n"..
+ "There's a base force and an added xyz vector force. You have options to choose how they're applied. Aside from that, the part's area is mainly for detection.\n\n"..
+ "For the Base force, Radial is from to self to each entity, Locus is from locus to each entity, Local is forward of self\n\n"..
+ "For the Vector force, Global is on world coordinates, Local is on self's coordinates, Radial is relative to the line from the self or locus toward the entity (Used in orbits/vortex/circular motion with centrifugal force)\n\n"..
+ "NPCs might have weird movement so don't expect much from pushing them."
+ },
+
+ ["faceposer"] = {
+ tooltip = "Adjusts multiple facial expression slots",
+ popup_tutorial =
+ "This part gives access to multiple facial expressions defined by your model's shape keys in one part.\n"..
+ "The flex multiplier affects the whole model, so you should avoid stacking faceposers if they have different multipliers."
+ },
+
+ ["command"] = {
+ tooltip = "Runs a console command or lua code",
+ popup_tutorial = "This part attempts to run a command or Lua code on your client. It may or may not work depending on the command and some servers don't allow you to run clientside lua, because of sv_allowcslua 0.\n\n"..
+ "Some example lua bits:\n"..
+ "\tif LocalPlayer():Health() > 0 then print(\"I'm alive\") RunConsoleCommand(\"say\", \"I\'m alive\") end\n"..
+ "\tfor i=0,100,1 do print(\"number\" .. i) end\n"..
+ "\tfor _,ent in pairs(ents.GetAll()) do print(ent, ent:Health()) end\n"..
+ "\tlocal random_n = 1 + math.floor(math.random()*5) RunConsoleCommand(\"pac_event\", \"event_\"..random_n)"
+
+ },
+
+ ["weapon"] = {
+ tooltip = "configures your weapon entity",
+ popup_tutorial = "This part is like an entity part, but for weapons. It can change your weapon's position and appearance, for all or one weapon class."
+ },
+
+ ["woohoo"] = {
+ tooltip = "applies a censor square",
+ popup_tutorial =
+ "This part draws a pixelated square with what's behind it, with a possible blur filter and adjustable resolution.\n"..
+ "It requires a lot of resources to set up and needs to refresh in specific circumstances, which is why you can't change its resolution or blur filtering state with proxies."
+ },
+
+ ["flex"] = {
+ tooltip = "Adjusts one facial expression slot",
+ popup_tutorial =
+ "This part gives access to one facial expression defined by your model's shape keys."
+ },
+
+ ["particles"] = {
+ tooltip = "Emits particles",
+ popup_tutorial =
+ "Throws particles into the world. They are quite configurable, can be flat 3D or 2D sprites, can be stretched with start/end length.\n"..
+ "To start with, you may want to set zero angle to false and particle angle velocity to (0, 0, 0)\n"..
+ "You can use a web texture but you might still need to work around material limitations for transparent images\n"..
+ "They are not PCF effects though. But I think that with a wise choice and layered particles, you can recreate something that looks like an effect."
+ },
+
+ ["custom_animation"] = {
+ tooltip = "sets up an editable bone animation",
+ popup_tutorial =
+ "This part creates a custom animation with a separate editor menu. It is not a sequence, but it moves bones on top of your base animations. It morphs between keyframes which correspond to bones' positions and angles. This is what creates movement.\n\n"..
+ "Custom animation types:\n"..
+ "\tsequence: loopable. plays the A-pose animation as a base, layers bone movements on top."..
+ "\tstance: loopable. layers bone movements on top."..
+ "\tgesture: not loopable. layers bone movements on top. ideally you should start with duplicating your initial frame once for smoothly going back to 0."..
+ "\tposture: only applies one non-moving frame. this is like a set of bones.\n\n"..
+ "There are interesting Easing styles available when you select the linear interpolation mode. They're useful in many ways, if you want to have more control over the dynamics and ultimately give character to your animation.\n"..
+ "While this is not the place to write a full tutorial for how to animate, or explaining animation principles in depth, I editorialize a bit and say those are two I try to aim for:\n"..
+ "\tinertia: trying to carry some movement over from a previous frame, because real physics take time to decelerate and accelerate between positions.\n"..
+ "\texaggeration: animations often use unnatural movement dynamics (e.g. different speeds at different times) to make movements look more pleasing by giving it more character. This goes in hand with anticipation."
+ },
+
+ ["beam"] = {
+ tooltip = "draws a rope or beam",
+ popup_tutorial =
+ "This part renders a rope or beam between itself and the end point. It can bend relative to the two endpoints' angles.\n"..
+ "frequency determines how many half-cycles it goes through. 1 is half a cycle (1 bump), 2 is one cycle(2 bumps)\n"..
+ "resolution is how many segments it tries to draw for that.\n\n"..
+ "And here's another reminder that while it can load url images, there are limitations so you may have to do something with a material part or blend mode if you want a custom transparent texture."
+ },
+
+ ["animation"] = {
+ tooltip = "plays a sequence animation",
+ popup_tutorial =
+ "This part plays a sequence animation defined in your model via the model's inherent animation definitions, included animations and active addons. Cannot load custom animations, not even .ani, .mdl or .smd\n"..
+ "If you want to import an animation from somewhere else, you need to know some decompiling/recompiling QC knowledge"
+ },
+
+ ["player_movement"] = {
+ tooltip = "edits your player movement",
+ popup_tutorial = "This part tells the server to handle your movement manually with a Move hook.\n"..
+ "Z-Velocity means you can move in the air relative to your eye angles, with WASD and jump almost like noclip. It is however still subject to air friction (needs friction to move, but friction also decelerates you) and uses ground friction as a driver.\n"..
+ "Friction generally cuts your movement as a percentage every tick. This is why it's very sensitive because its effect is exponential. Horizontal air friction tries to mitigate that a bit\n"..
+ "Reverse pitch is probably buggy. "
+ },
+
+ ["group"] = {
+ tooltip = "organizes parts",
+ popup_tutorial =
+ "This part groups parts. That's all it does. It bypasses parenting, which means it has no side effect, aside from when modifiers act on their direct parent, in which case the group can get in the way.\n"..
+ "But with a root group, (a group at the root/top level, \"my outfit\"), you can choose an owner name to select another entity to bear the pac outfit."
+ },
+
+ ["lock"] = {
+ tooltip = "grabs or teleports",
+ popup_tutorial =
+ "This part allows you to grab things or teleport yourself.\n\n"..
+ "Warning in advance: It has the most barriers because it probably has the most potential for abuse out of all parts.\n"..
+ "\tClients need to give consent explicitly (pac_client_grab_consent 1), otherwise you can't grab them.\n"..
+ "\tThis is doubly true for players' view position. That's another consent (pac_client_lock_camera_consent 1) layered on top of the existing grab consent.\n"..
+ "\tOn top of that, grabbed players will get a notification if you grab them, and they will know how to break the lock. Clients have multiple commands (pac_break_lock, pac_stop_lock) to request the server to force you to release them. It is mildly punitive.\n"..
+ "\tThere are multiple server-level settings to limit it. Some servers may even wholesale disable the new combat parts for all players by default until they're trusted/whitelisted.\n\n"..
+ "Now, here's business. How it works, and how to use this part:\n"..
+ "\tThe part searches for entities around a sphere, selects the closest one and locks onto it. You should plan ahead for the fact that it only picks up entities by their origin position, which for NPCs and players is between their feet. offset down amount compensates for this, but only for where the detection radius begins.\n"..
+ "\tIt will then start communicating with the server and the server may reposition the entity if it's allowed. If rejected, you may get a warning in the console, and the part will be stopped for a while."..
+ "\tOverrideEyeAngles works for players only, and as stated previously, is subject to consent restrictions.\n"
+ },
+
+ ["physics"] = {
+ tooltip = "creates a clientside physics object",
+ popup_tutorial =
+ "This part creates a physics object clientside which can be a box or a sphere. It will relocate the model and pac parts contained and put them in the object.\n"..
+ "It's not compatible with the force part, unfortunately, because it's clientside. There are other reasonably fun things it can do though.\n"..
+ "It only works as a direct modifier on a model."
+ },
+
+ ["jiggle"] = {
+ tooltip = "wobbles around",
+ popup_tutorial =
+ "This part creates a subpoint that carries base_movables, and moves around with a certain type of dynamics that can lag behind and then catch up, or wiggle back and forth for a while. Strain is how much it will wobble. The children parts will be contained within that subpoint.\n"..
+ "There is immense utility to control movement and have some physicality to your parts' movement. To name a few examples:\n"..
+ "\tThe jiggle 0 speed trick: Having your jiggle set at 0 speed will freeze what's inside. You can easily control that with two proxies: one for moving (not 0), one for stopping (0)\n"..
+ "\tPets and drones: Fun things that are semi-independent. Easy to do with jiggle.\n"..
+ "\tSmoother transitions with multiple static proxies: If you have position proxies that snap to different positions, making a model teleport too fast, using these proxies on a jiggle instead will let the jiggle do the work of smoothing things out with the movement.\n"..
+ "\tForward velocity indicator via a counter-lagger: jiggle lags behind an origin, model points to origin with aim part, other model is forward relative to the pointer. Result: a model that goes in the direction of your movement.\n\n"..
+ "The part, however, has issues when crossing certain angles (up and down)."
+ },
+
+ ["projected_texture"] = {
+ tooltip = "creates a lamp",
+ popup_tutorial =
+ "This part creates a dynamic light / projected texture that can project onto models or the world. That's pretty much it. It's useful for lamps, flashlights and the like.\n"..
+ "But if you're expecting a proper light, its directed lighting method gives mediocre results alone. With another light, and a sprite maybe, it'll look nicer. We won't have point_spotlights though.\n"..
+ "Its brightness works by multiplication, not magnitude. 0 is a proper 0 amount.\n\n"..
+ "Because it uses ITexture / VTF, it doesn't link up with pac materials. Animated textures must be done by frames instead of proxies. Although you can still set a custom image. But it's additive so the transparency can't be done with alpha, but on a black background\t"..
+ "fov on one hand, and horizontal/vertical fovs on the other hand, compete; so you should touch only one and leave the other."
+ },
+
+ ["hitscan"] = {
+ tooltip = "fires bullets",
+ popup_tutorial =
+ "This part tries to fire bullets. There are damaging serverside bullets and non-damaging clientside bullets. Both could be useful in their own scenarios.\n"..
+ "For serverside bullets, the server might restrict that. For example, it can force you to spread your damage among all your bullets, to notably prevent you from stacking tons of bullets to multiply your damage beyond the limit.\n"..
+ "Damage falloff works with a fractional floor on individual bullets, which means each bullet is lowered to a percentage of its max damage."
+ },
+
+ ["motion_blur"] = {
+ tooltip = "makes a trail of after-images",
+ popup_tutorial =
+ "This part continually renders a series of ghost copies of your model along its path to simulate a motion blur-like effect.\n"..
+ "It has limited options because of how models' clientside entity is set up, allegedly."
+ },
+
+ ["link"] = {
+ tooltip = "transfers variables between parts",
+ popup_tutorial =
+ "This part tries to copy variables between two parts and update them when their values change.\n"..
+ "It doesn't work for all variables especially booleans! Also, \"link\" is a strong word. Whatever you think it means, it's not doing that."..
+ "Might require a rewear to work properly."
+ },
+
+ ["effect"] = {
+ tooltip = "runs a PCF effect",
+ popup_tutorial =
+ "This part uses an existing PCF effect on your game installation, from your mounted games or addons. No importable PCFs from the web.\n"..
+ "It apparently can use control points and make tracers work. It may or may not be supported by different effects; start by putting the effect in a basic model to position the effect.\n"..
+ "And PCF effects can be a gigantic pain, with for example looping issues, permanence issues (BEWARE OF TF2 UNUSUAL EFFECTS!), wrong positions etc."..
+ "You should probably look into particles and think about how to layer them if you're looking for something more configurable."
+ },
+
+ ["text"] = {
+ tooltip = "draws 3D2D or 2D text",
+ popup_tutorial =
+ "This part renders text on a flat surface (3D2D with the DrawTextOutlined mode) or on the screen (2D with the SurfaceText mode). Due to technical limitations, some features in one may not be present in the other, such as the outline and the size scaling\n\n"..
+ "You can use a combination of data and text to build up your text. Combined text tells the part to use both, and text position tells you whether the text is before or after the data.\n"..
+ "What's this data? text override. There are a handful of presets, like your health, name, position. If you want more control, you can use Proxy, and it will use the dynamic text value (a simple number variable) which you can control with proxies.\n\n"..
+ "If you want to raise the resolution of the text, you should try making a bigger font. But creating fonts is expensive so it's throttled. You can only make one every 3 seconds.\n"..
+ "Although you can use any of gmod's or try to use your operating system's font names, there are still limits to fonts' features, both in their definitions and in the lua code. Not everything will work. But it will create a unique ID for the font it creates, and you can reuse that font in other text parts."
+ },
+
+ ["camera"] = {
+ tooltip = "changes your view",
+ popup_tutorial =
+ "This part runs a CalcView hook to allow you to go into a third person mode and change your view accordingly. Some parts on your player may get in the way.\n"..
+ "eye angle lerp determines how much you mix the original eye angles into the view. Otherwise at 0 it will fully use the part's local angles.\n"..
+ "Remember a right hand rule! Point forward, thumb up, middle finger perpendicular to the palm. This is how the camera will look with 0 lerp.\n"..
+ "\tX = Red = index finger = forward\n"..
+ "\tY = Green= middle finger = left\n"..
+ "\tZ = Blue = thumb finger = up\n"..
+ "As an example, if you apply this, you will learn that, on the head, you can simply take a 0,-90,-90 angle value and be done with it.\n\n"..
+ "Because of how pac3 works, you should be careful when toggling between cameras. I've made some fixes to prevent part of that, but if you hide with events, and lose your final camera, you can't come back unless you go back to third person (to restart the cameras) and then back into first person (to put priority back on an active camera)."
+ },
+
+ ["decal"] = {
+ tooltip = "applies decals",
+ popup_tutorial =
+ "Decals are a Source engine thing for bullet holes, sprays and other such flat details as manholes and posters. This part when shown emits one by tracing a line forward and applying it at the hit surface.\n"..
+ "It can use web images and pac materials, but still subject to rendering quirks depending on transparency and others.\n"..
+ "Decals are semi-permanent, you can only remove them with r_cleardecals"
+ },
+
+ ["projectile"] = {
+ tooltip = "throws missiles into the world",
+ popup_tutorial =
+ "the projectile part creates physical entities and launches them forward.\n"..
+ "the entity has physics but it can be clientside (visual) or serverside (physical)\n"..
+ "by selecting an outfit part, the entity can bear a PAC3 part or group to have a PAC3 outfit of its own\n"..
+ "the entity can do damage but servers can restrict that.\n"..
+ "For visual reference, a 1x1x1 cubeex is around radius 24, and a default pac sphere is around 8."
+ },
+
+ ["poseparameter"] = {
+ tooltip = "sets a pose parameter",
+ popup_tutorial =
+ "pose parameters are a Source engine thing that helps models animate using a \"blend sequence\". For instance, this is how the body and head are rotated, and how 8-way walks are blended.\n"..
+ "It goes without saying that not all models have those, and some have fewer supported pose parameters because of how they were made."
+ },
+
+ ["entity"] = {
+ tooltip = "edits an entity (legacy)",
+ popup_tutorial = "The legacy entity part still does the usual things you need to edit your entity. Color, model, size, no draw etc. But better use the new entity part."
+ },
+
+ ["entity2"] = {
+ tooltip = "edits an entity",
+ popup_tutorial =
+ "This part can edit some properties of an entity. This can be your playermodel, a pac3 model (it's a clientside entity) or a prop (you give a prop a pac outfit by selecting it with the owner name on a root group).\n"..
+ "It supports web models. See the model part's tutorial or read the MDL zips page on our wiki for further info.\n\n"..
+ "As with other bone-related things, it might not work properly if you use it on a ragdoll or some similar entities.\n\n"..
+ "Another warning in advance, if you wonder why your playermodel won't change, there are some addons, such as Enhanced Playermodel Selector, known to cause issues because they override your entity, thus conflicting with pac3. This one can be fixed if you disable \"enforce playermodel\"\n"..
+ "Other than that, the server setting pac_modifier_model and pac_modifier_size can forbid you from changing your playermodel and size respectively."
+ },
+
+ ["interpolated_multibone"] = {
+ tooltip = "morphs position between nodes",
+ popup_tutorial =
+[[This part repositions its contents according to a path with multiple nodes. It blends the position and angle by mixing those of the current node with those of the next node.
+Obviously enough, the nodes you select need to be base_movable parts.
+
+
+As you may know, like with jiggles, it's like making a container inside a part, the action happens when the container moves in a special way.
+Jiggles and interpolators' "containers" are dynamic. Unlike models', which are just located on the parent model itself.
+Jiggles would be as if the container is attached with springs all around, that's how it jiggles around.
+The interpolator would be as if the container is a cart on a rail, going to different places.
+
+
+The "first" / prime / home / origin (Zeroth) node is the interpolator itself. When lerp value is 0, this is where the container will be.
+It's useful to start near zero if you want to create interpolators dynamically, as with projectiles. Or to simply have a reference point.
+
+The first outside node, part1, will be used if lerp value is more than 0. At 1 the container will be located at part1. at 0.5, it would be halfway between home and part1.
+The second outside node, part2, will be used if lerp value is more than 1. At 2 the container will be located at part2. at 1.5, it would be halfway between part1 and part2.
+and so on.
+
+
+In terms of how it works as a base_movable, it's decent enough but you should still be aware of some things, perhaps as a reminder.
+If you want to adjust the thing inside, think about where it'll end up. Adjustments can be made on the prime node, the subsequent nodes, the contained parts themselves or even on the path itself.
+
+If the angle or position to adjust depends on only one node, it's probably a good idea to make your adjustment on the node in question. As long as your offsets on the contained parts aren't too strong, its angle should be representative enough.
+Why this matters is that you may think it looks good now, but if you offset too much on the contained parts, the offsets will still be there on the other nodes.
+
+Also, with an interpolation very close to one node, try not to overcorrect. If it takes 90 degrees to tweak the appearance just a little bit, maybe it's a good idea to think about the path itself instead of the node.
+If you do this haphazardly, it will mess up other interpolators or parts that rely on that node.
+Maybe your knee should've been more in 0.5 ranges instead of pushing excessive angles on the other end that get reduced by the fact the lerp value is like 0.2.
+Any adjustment you make is only gonna show up as much as the morphing progress allows it.
+
+Now for most users, you can just keep playing around with it and find out that maybe you'd prefer adjusting the nodes instead, or instead tweaking the way your lerp value is set up. Or that a slight offset is fine once it shows on all nodes.
+
+
+There may be some issues with how the angles are calculated, and there's the occasional issue with base_movable lagging but you can hack some temporary fixes by checking translucent on some things.
+
+
+Suggested uses for this part:
+camera with multiple positions for a cutscene
+joints : like a kneepad or some other articulated bit between two moving parts/bones
+returning hitpos pseudo-projectile : like a boomerang
+reposition pets
+crazy position randomizer: position a part on random positions]]
+
+ },
+
+ ["proxy"] = {
+ tooltip = "applies math to parts",
+ popup_tutorial =
+[[This part computes math and applies the numbers it gives to a parameter on a part, for number (x), vector (x,y,z) or boolean(true (1) or false (0)) types. It can send to the parent, to all its children, or to an external target part.
+Easy setup can help you make a rough idea quickly, but writing math yourself in the expression gives supremely superior control over what the math does.
+
+
+Here's a quick crash course in the syntax with basic examples showing the rules to observe:
+
+Basic numbers /math operators : 4^0.5 - 2*(0.2 / 5) + timeex()%4
+The only basic operators are: + - * / % ^
+
+Functions:
+ Functions are like variables that gather data from the world or that process math.
+ Most functions are nullary, which means they have no argument: timeex(), time(), owner_health(), owner_armor_fraction()
+ Others have arguments, which can be required or optional: clamp(x,min,max), random(), random(min,max), random_once(seed,min,max), etc.
+ All Lua functions are declared by a set of parentheses containing arguments, possibly separated by commas.
+
+Arguments and tokens:
+ Most arguments' type is numbers, but some might be strings with some requirements; Most of the time it's a name or a part UID, for example:
+ Valid number arguments are numbers, functions or well-formed expressions. It's the same type because at the end of the day it gives you a number.
+ Needless to say, if you compose an expression, you need a coherent link between the tokens (i.e. math operators or functions). 2 + 2 is valid, 2 2 is not.
+ Valid string arguments are text declared by double quotes. Lua's string concatenation operator works. command("name"..2) is the same as command("name2")
+ Without the string declaration, Lua tries to look for a global variable. command("name") is valid, command(name) is not.
+
+Nested functions (composition) : clamp(1 - timeex()^0.5,0,1)
+ As you can see, you can have functions inside of functions.
+
+XYZ / Vectors (comma notation) : 100,0,50
+ vectors for position, angles, colors etc. are written that way.
+
+nil (skipping an axis) : 100,nil,0
+ if you have an "invalid" variable name like our commonly-used dummy nil, the proxy will leave that axis unchanged, so you can adjust the value manually or with another proxy.
+ you could also set your axis in easy setup.
+
+You can write pretty much any math using the existing functions as long as you observe the syntax's rules: the most common ones being to close your brackets properly, don't misspell your functions' names and give them all their necessary arguments.
+
+There are lots of technical things to learn. Do not be overwhelmed. Feed your curiosity instead. Documentation is NOT lacking.
+I wrote builtin tutorials for pretty much every last function. It's accessible pretty much everywhere right from the editor.
+
+By right clicking the expression field, you can consult my example proxy bank. You will also see options for tutorials for any active function on that proxy.
+
+Do that, or use the "view specific help or info about this part" option right clicking on the part once you've selected an input or written some functions.
+
+Better yet, opening the inputs list will even have these tutorial entries as tooltips!
+
+Go consult our wiki for reference. https://wiki.pac3.info/part/proxy
+It's not fully up to date on new functions because it reflects the main version, but you're currently on develop. The function tutorials are available in the input list.
+
+
+As a conclusion, I'm gonna editorialize and give my recommendations:
+ Write with purpose. Avoid unnecessary math.
+ ->But still, write in a way that lets you understand the concept. It's not bad to have an imperfect expression.
+ ->This is why I added support for names for uid-based functions. var1("fade_factor") is more
+
+ -More to the point, please have patience and deliberation. Make sure every piece works BEFORE moving on and making it more complex.
+ ->A very common problem that people do is they add stuff randomly by cobbling stuff together not knowing why. Please don't do things haphazardly.
+ ->Blind faith will cause problems for exactly that reason.
+
+ -The fundamental mechanism of developing and applying new ideas is composition / compounding.
+ ->Multiplying different expression bits together tends to combine the concepts.
+ ->e.g. ezfade(0.2)*sin(10*time()) is a fadein and a sine wave. What do you get? sine wave fading to full power.
+
+ -Please read the debug messages in the console or in chat. They will help the correction process.
+
+ -You know where to look for help. With a good enough topic, we can discuss math at length. Why else would they have whole university courses for real math? Think about it. There's lots of ways to approach it.
+ ->But I can't help you if you're not curious. I can only hope that you read through this without skipping to the end. But my faith isn't worth much.]]
+ },
+
+ ["sunbeams"] = {
+ tooltip = "shines like rays of light",
+ popup_tutorial =
+ "This part applies a sunbeam effect centered around the part.\n"..
+ "Multiplier is the strength of the effect. It can also be negative for engulfing darkness.\n"..
+ "Darken is how much to darken or brighten the whole effect. It helps control the contrast in conjunction with multiplier. With enough darken, only the brightest rays will go through, otherwise with unchecked multipliers or negative darken there's a whole blob of white that just overpowers everything\n"..
+ "Size affects the after-images projected around the center, which serve as a base for the effect."
+ },
+
+ ["shake"] = {
+ tooltip = "shakes nearby viewers\' camera",
+ popup_tutorial =
+ "This part applies a shake that uses the camera\'s position to take effect. For that reason, it may be nullified by certain third person addons, as well as the pac3 editor camera. You can still temporarily disable it to preview your shakes."
+ },
+
+ ["gesture"] = {
+ tooltip = "plays a gesture",
+ popup_tutorial =
+ "Gestures are a type of animation usually added as a layer on top of other animations, this part tries to play one but not all animations listed are gestures, so it might not work for most."
+ },
+
+ ["damage_zone"] = {
+ tooltip = "deals damage in a zone",
+ popup_tutorial =
+[[This part can deal hitbox-based damaged via the server. It might not be allowed. There are server settings (e.g. pac_sv_damage_zone) and client consents (pac_client_damage_zone_consent) for protecting the peace.
+Server owners can add or remove entity classes that can be damaged with pac_damage_zone_blacklist_entity_class, pac_damage_zone_whitelist_entity_class commands.
+
+Most hitbox shapeas should be self-explanatory, but you can use the preview function to see what it should cover.
+There are some settings for raycasts which could come in handy for some niche use cases, but you'll probably use one of the basic ones (box, sphere, cone from spheres, ray)
+you can filter by certain types of targets like general classes like NPCs, which should include VJ and DRG base, as well as according to their "friendliness" which are related to dispositions (ally, enemy, neutral).
+
+Damage falloff:
+reduces the damage depending on the distance (according to the hitbox shape), and you can set a power for a bumpier or sharper falloff
+
+Do Not Kill and Reverse Do Not Kill:
+critical health is a point of comparison.
+Do Not Kill prevents the damage zone from taking health below that point. when healing, it prevents from healing above that point. converge to the critical health.
+Reverse Do Not Kill damages only if health is below critical health, heals only if health is above critical health. diverge from the critical health.
+
+Damage Scaling:
+damage scaling applies a maxHP%-type damage, a fraction of max HP. 1 is 100% of HP. if a target has 150 HP and 100 max HP, it will deal 100 damage.
+it doesn't always insta-kill, as damage multipliers can still take effect, but it's there to let you even the playing field with any NPC like nextbots with absurd HP
+
+There are certain "special" damage types. the dissolves can disintegrate entities but can be restricted in the server, prevent_physics_force suppresses the corpse force, removenoragdoll removes the corpse.
+You can define your damage as a damage over time, which repeats the same damage a number of times. DOT time is the delay between ticks, DOT count is the number of ticks.
+
+You can link the damage zone to sounds to use as hit sounds or kill sounds. They should work without much issue.
+You can set hit parts, which are like projectiles. they spawn on targets. It can be unreliable though!]]
+ },
+
+ ["health_modifier"] = {
+ tooltip = "modifies your health, armor",
+ popup_tutorial =
+[[This part lets you change your max health, armor, add a multiplier for damage taken, and create extra health bars that take damage before the main health.
+
+the "follow health" and "follow armor" checkboxes mean the current health will follow your max health or armor on specific conditions.
+It will increase alongside the max IF you're already at the max. And it will be brought down if you lower the max below what you currently have.
+
+For the extra bars, you can set a layer priority to pick which ones get damaged first. higher layer means it gets damaged first.
+View the info bubble to view the part's current extra health.
+available events or proxies for visualization:
+healthmod_bar_hit detects when it gets updated
+healthmod_bar_layertotal gets the total value for one layer
+healthmod_bar_total gets the total value across all layers and parts
+healthmod_bar_uidvalue gets the value of one part as an overall amount
+healthmod_bar_remaining_bars(uid) gets the value of one part as a number of bars.
+
+Absorb Factor is a multiplier to the damage that goes to the main health when the extra bars get damaged.
+1 will make the extra health basically ineffective, 0 is normal, -1 will heal the main health for every damage received.
+
+The part's usage may or may not be allowed by the server.]]
+ },
+
+ ["mesh_trail"] = {
+ tooltip = "produces a trail as a model",
+ popup_tutorial =
+[[This part continuously creates a trail of segments linked as a continuous model. You'll see how it differs from beam-based trails.
+
+end position side represents where the trail's end tapers off, 1 is the tip, 0 is the center, and -1 the base
+
+you should know U and V are a vertex/point's texture coordinates, the trail can be customized with these values. U generally stretches out, V results in a shear
+
+This is still a work in progress. Only the rod mode is implemented, and the basis modes aren't implemented except time
+
+if you are using a custom texture, you can try to add "noclamp " before your link]]
+ },
+
+
+ }
+
+ for i,v in pairs(pace.TUTORIALS.PartInfos) do
+ if pace.PartTemplates then
+ if pace.PartTemplates[i] then
+ pace.PartTemplates[i].TutorialInfo = v
+ end
+ end
+ end
+end
+
+
diff --git a/lua/pac3/editor/client/proxy_function_tutorials.lua b/lua/pac3/editor/client/proxy_function_tutorials.lua
new file mode 100644
index 000000000..34aa475f9
--- /dev/null
+++ b/lua/pac3/editor/client/proxy_function_tutorials.lua
@@ -0,0 +1,742 @@
+local Tutorials = {}
+
+--basic math functions
+Tutorials["none"] = [[none(n) doesn't do anything. it passes the argument straight through. it's only used in easy setup to read an input without modifications.]]
+Tutorials["sin"] = [[sin(rad) is the sine wave function. it has a range of [-1,1] and is cyclical.
+
+rad is radians. one full cycle takes 2*PI radians. pi is 3.1416... so a full cycle is around 6.283 radians
+
+sin(0) = 0 (zero crossing on the upward slope)
+sin(PI/2) = 1 (the peak)
+sin(PI) = 0 (zero crossing on the downward slope)
+sin((3/2) * PI) = -1 (the trough / valley)
+sin(2*PI) = 0 (zero crossing on the upward slope, starting another cycle)
+
+there are some interesting symmetries and regularities, but either you already know what a sine is, or you don't. I'll just give you some interesting setups
+
+the most typical use involves time.
+sin(time()*10)
+
+UCM (uniform circular motion)
+when you map a sine and cosine on the position of an object, you trace a circular path by definition. the sine is the height or Y, the cosine is the width or X position.
+it's useful for orbits / revolutions
+100*sin(3*time()), 100*cos(3*time())
+
+power sines make shorter pulses. even powers make the range [0,1] (they convert every trough into a peak), while odd powers keep the range to [-1,1].
+compare these:
+sin(time()*5)^10
+sin(time()*5)^15
+
+also see nsin and nsin2]]
+
+Tutorials["cos"] = [[cos(rad) is the cosine wave function. it has a range of [-1,1] and is cyclical.
+
+rad is radians. one full cycle takes 2*PI radians. pi is 3.1416... so a full cycle is around 6.283 radians
+
+sin(0) = 1 (the peak)
+sin(PI/2) = 0 (zero crossing on the downward slope)
+sin(PI) = -1 (the trough / valley)
+sin((3/2) * PI) = 0 (zero crossing on the upward slope)
+sin(2*PI) = 1 (the peak)
+
+there are some interesting symmetries and regularities, but either you already know what a cosine is, or you don't. I'll just give you some interesting setups
+
+the most typical use involves time.
+cos(time()*10)
+
+UCM (uniform circular motion)
+when you map a sine and cosine on the position of an object, you trace a circular path by definition. the sine is the height or Y, the cosine is the width or X position.
+it's useful for orbits / revolutions
+100*sin(3*time()), 100*cos(3*time())
+
+power sines make shorter pulses. even powers make the range [0,1] (they convert every trough into a peak), while odd powers keep the range to [-1,1].
+compare these:
+cos(time()*5)^10
+cos(time()*5)^15
+
+also see ncos and ncos2]]
+
+Tutorials["tan"] = [[tan(rad) is the tangent function. I don't have much to say about it but you can look more up if you wish.
+the range is [-inf,inf], it's asymptotic, it represents the slope of a surface if it's perfectly vertical, the slope is practically infinite.
+
+rad is radians. one full cycle takes 2*PI radians. pi is 3.1416... so a full cycle is around 6.283 radians
+
+]]
+Tutorials["abs"] = [[abs(n) takes the absolute value. it removes any negative sign. that's all. it's useless if you're always working with positive numbers.]]
+Tutorials["sgn"] = [[sgn(n) takes the sign.
+if n > 0 then sgn(n) = 1
+if n = 0 then sgn(n) = 0
+if n < 0 then sgn(n) = -1
+
+idea: you can use sgn with random_once to randomly pick a side with sgn(random_once(0,-1,1)), then multiplying with whatever else you might've had.]]
+Tutorials["acos"] = [[acos(cos) is the arc-cosine, the reverse of cos. it will give the corresponding angle in radians.
+cos is a cosine value. we expect between -1 and 1]]
+Tutorials["asin"] = [[asin(sin) is the arc-sine, the reverse of sin. it will give the corresponding angle in radians.
+sin is a sine value. we expect between -1 and 1]]
+Tutorials["atan"] = [[atan(tan) is the arc-tangent, the reverse of tan. it will give the corresponding angle in radians.
+sin is a tangent value]]
+Tutorials["atan2"] = [[atan2(tan) is an alternate arc-tangent, the reverse of tan. it will give the corresponding angle in radians.
+tan is a tangent value]]
+Tutorials["ceil"] = [[ceil(n) rounds the number up.
+ceil(0) = 0
+ceil(0.001) = 1
+ceil(1) = 1]]
+Tutorials["floor"] = [[floor(n) rounds the number down.
+floor(0) = 0
+floor(0.999) = 0
+floor(1) = 1]]
+Tutorials["round"] = [[round(n,dec) rounds the number up to a certain amount of decimals
+dec is decimal magnitude. 0 if not provided (whole numbers), 1 is tenths, 2 is hundredths, -1 is tens, -2 is hundreds etc.]]
+Tutorials["rand"] = [[rand() is math.random. it generates a random number from 0 to 1]]
+Tutorials["randx"] = [[randx(a,b) is math.Rand(a,b). it generates a random number from a to b.]]
+Tutorials["sqrt"] = [[sqrt(x) is just the square root. it's equivalent to x^0.5
+avoid negative values.]]
+Tutorials["exp"] = [[exp(base,exponent) is an exponentiation. it's equivalent to base^exponent]]
+Tutorials["log"] = [[log(x, base) is the logarithm on a base. logarithms are the reverse of the exponentiation operation.
+e.g. since 10^3 = 1000, log(1000,10) = 3]]
+Tutorials["log10"] = [[log10(x) is the logarithm on base ten. logarithms are the reverse of the exponentiation operation.
+e.g. since 10^3 = 1000, log10(1000) = 3]]
+Tutorials["deg"] = [[deg(rad) converts radians to degrees. PI radians = 180 degrees]]
+Tutorials["rad"] = [[rad(deg) converts degrees to radians. PI radians = 180 degrees]]
+Tutorials["clamp"] = [[clamp(x,min,max) restricts x within a minimum and maximum. if x goes above max, clamp will still return max. if x goes below min, clamp will still return min.
+observe clamp(timeex(),0,1) or clamp(10*timeex(),0,50)
+it was standard for fades and movement transitions but now ezfade and ezfade_4pt exist to make it easier]]
+
+Tutorials["nsin"] = [[nsin(radians) is the normalized sine.
+it is simply 0.5 + 0.5*sin(radians)
+
+whereas sin has the codomain of [-1,1], we may sometimes want a normalized [0,1] for various reasons
+
+keep in mind sin(0) is 0 (the wave's zero-crossing going up), so nsin(0) will be 0.5, so you may want to use nsin2 if you want to start at 0]]
+
+Tutorials["nsin2"] = [[nsin2(radians) is another normalized sine, but phase-shifted to start at 0.
+it is simply 0.5 + 0.5*sin(-PI/2 + radians)
+
+whereas sin has the codomain of [-1,1], we may sometimes want a normalized [0,1] for various reasons
+
+keep in mind sin(0) is 0 (the wave's zero-crossing going up), so nsin(0) will be 0.5, this is why nsin2 exists to start at 0 (the wave's trough) instead]]
+
+Tutorials["ncos"] = [[ncos(radians) is the normalized cosine.
+it is simply 0.5 + 0.5*cos(radians)
+
+whereas cos has the codomain of [-1,1], we may sometimes want a normalized [0,1] for various reasons
+
+keep in mind sin(0) is 0, so nsin(0) will be 0.5, so you may want to use ncos2 if you want to start at 0]]
+
+Tutorials["ncos2"] = [[ncos2(radians) is another normalized cosine, but phase-shifted to start at 0.
+it is simply 0.5 + 0.5*sin(-PI + radians)
+
+whereas cos has the codomain of [-1,1], we may sometimes want a normalized [0,1] for various reasons
+
+keep in mind cos(0) is 1 (the wave's peak), so ncos(0) will be 0.5, this is why you should use ncos2 if you want to start at 0 (the wave's trough)
+but ncos2 is the same as nsin2. we forced them to have the wave starting at the same phase]]
+
+Tutorials["polynomial"] =
+[[polynomial(x, a0, a1, a2, a3 ..., aN)
+
+computes a polynomial series, which means it takes the base x and sums over exponents for every coefficient provided a1*x + a2*x^2 + a3*x^3 ... + aN*x^N
+x is the base
+for any a(0) .. to a(N), a(n) is a coefficient and the sum will add a * x^n
+
+e.g. you might have polynomial(2,0,1,2,3) which is 34, since it is computed as 0*1 + 1*2 + 2*(2*2) + 3*(2*2*2)]]
+
+
+--basic logic and commands
+Tutorials["command"] = [[command(name) reads your own pac_proxy data (from console commands).
+
+name is the name of the value. it is optional but the alternative is weird.
+without that argument, it will use the name of the proxy part. which wouldn't allow multiple values in the same expression.
+
+e.g. "pac_proxy my_number 1" means command("my_number") will be 1
+if you then run "pac_proxy my_number ++1" repeatedly, command("my_number") will be 2, then 3, then 4 ...
+you can also do "pac_proxy my_number --5" etc. and enter vectors.
+"pac_proxy my_number 1 2 3"]]
+
+Tutorials["property"] =
+[[property(property_name, field)
+
+it takes a part's property. the part is the target entity
+
+property_name is a part's variable name. e.g. "Alpha"
+field is an axis: "x", "y", "z", "p", "y", "r", "r", "g", "b"]]
+
+Tutorials["number_operator_alternative"] = [[number_operator_alternative(comp1, op, comp2, num1, num2) or if_else is a simple if statement to choose between two numbers depending on the compared input values
+
+comp1 is the first element to compare
+op is the operator, it is a string / text. we expect quotes. you have most number-based operators written different ways
+ "=", "==", "equal"
+ ">", "above", "greater", "greater than"
+ ">=", "above or equal", "greater or equal", "greater than or equal"
+ "<", "below", "less", "less than"
+ "<=", "below or equal", "less or equal", "less than or equal"
+ "~=", "!=", "not equal"
+
+comp2 is the second element to compare.
+
+num1 is the result to give if the comparison was found to be true. that's the "if" case, it's optional, 1 if not provided
+num2 is the result to give if the comparison was found to be false. that's the "else" case, it's optional, 0 if not provided
+
+thus we might have if_else(4, ">", 5, 1, -1), and we know that's not true so we shall take -1 as a result]]
+Tutorials["if_else"] = Tutorials["number_operator_alternative"]
+
+Tutorials["sequenced_event_number"] = [[sequenced_event_number(name) reads your own pac_event_sequenced data for command events (from console commands).
+
+name is the base name of the sequenced event
+
+events will only be registered as sequences if you have a series of numbered command events like hat1, hat2, hat3,
+or if you force it to register with e.g. "pac_event_sequenced_force_set_bounds color 1 5"
+
+keep in mind sequenced events are managed independently from normal command events, although they end up applied to the same source.
+we will not change the code to force that. if you have a series of numbered events, they are not necessarily a sequence. e.g. I have togglepad0 to togglepad9 bound to my keypad, they are independent bind togglers, they shouldn't mess with each other.
+
+if they are a sequenced event, you should avoid triggering them from the event wheel or with normal pac_event commands. please use pac_event_sequenced to set your sequenced events so the function can update properly.]]
+
+Tutorials["feedback"] = [[feedback(), feedback_x(), feedback_y() and feedback_z() take the proxy's previous computed value of the main expression.
+it is used in feedback controllers to maintain a memory that's adjustable with a command-controlled speed, and in feedback attractors to gravitate toward a changeable target number
+
+typical examples would be
+feedback() + ftime()*command("speed")
+feedback_x() + ftime()*(command("targetx") - feedback_x()), feedback_y() + ftime()*(command("targety") - feedback_y()), feedback_z() + ftime()*(command("targetz") - feedback_z())
+feedback() - 4*(command("target") - feedback())
+
+extra expressions can't change feedbacks. they can read them from the previous frame though. feebacks are only computed from the main expression]]
+
+Tutorials["feedback_x"] = Tutorials["feedback"]
+Tutorials["feedback_y"] = Tutorials["feedback"]
+Tutorials["feedback_z"] = Tutorials["feedback"]
+
+local extravar_tutorial = [[the extra/var series range from 1 to 5, so you'll have extra1, extra2, extra3, extra4, extra5 or alternatively var1, var2, var3, var4, var5
+
+var1(uid) for example takes the result of the first extra expression of the proxy referenced
+
+uid is a string argument corresponding to the Unique ID, partial UID or name of a proxy.
+It's optional but you'll probably end up using it anyway because it's not hugely useful if it's gone.
+
+the two main uses for this function are:
+1-without the uid argument: working inside the same proxy, compressing some math for readability. extra expressions are computed before the main expression.
+2-with the uid argument: outsourcing / creating variables used by other proxies. defining some stuff outside is useful to make your proxies more meaningful and simpler down the line
+
+Keep in mind if you have feedback functions, feedback can only change based on the main expression. Put your main math in the main expression in that case.
+You can simply put a feedback() in your extra expression and it'll work then. You could also do some minor reformatting on it.
+
+here's what you should do, e.g. with a standard feedback attractor setup.
+main : feedback() + ftime()*(feedback)
+extra1 : feedback()]]
+
+for i=1,5,1 do
+ Tutorials["var"..i] = extravar_tutorial
+ Tutorials["extra"..i] = extravar_tutorial
+end
+
+
+--sequences
+Tutorials["hexadecimal_level_sequence"] = [[hexadecimal_level_sequence(freq, hex) converts a hexadecimal string into numbers, and animated as a sequence, but normalized to [0,1] ranges
+
+freq is the frequency of the sequence. how many times it should run every second.
+hex is a hexadecimal (base 16) string / text. every letter corresponds to a frame, these are divided by 15 at the end.
+
+it's a weird one but it was a coin flip to decide whether people want a true hexadecimal or a normalized sequence maker.
+
+"0" = 0
+"1" = 1/15 (0.067)
+...
+"9" = 9/15 (0.6)
+"a" = 10/15 (0.667)
+"b" = 11/15 (0.733)
+"c" = 12/15 (0.8)
+"d" = 13/15 (0.866)
+"e" = 14/15 (0.933)
+"f" = 1
+
+"0f" would be a simple binary flicker pulse
+"000f00000f00f00f0f0ff00f0f0f0f" would be a semi-erratic flicker, might look good with 0.5 frequency.
+"0123456789abcdefedcba987654321" would be a linear fadein-fadeout]]
+
+Tutorials["letters_level_sequence"] = [[letters_level_sequence(freq, str) converts a string of letters into numbers
+
+it's inspired by Source / Hammer light presets, but normalized to [0,1] ranges.
+
+freq is the frequency of the sequence. how many times it should run every second.
+str is the letters. we expect quotes.
+
+"az" would be a simple binary flicker pulse
+"abcdefghijklmnopqrstuvwxyz" would be a sawtooth-style pulse]]
+
+Tutorials["numberlist_level_sequence"] = [[numberlist_level_sequence(freq, ...) converts a "vararg" list of numbers and cycles through them regularly. no further processing is done to the numbers
+
+freq is the frequency of the sequence. how many times it should run every second.
+... is the numbers, separated by commas e.g. a1, a2, a3 ... aN
+
+0,1 would be a simple binary flicker pulse
+0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9,1 would be a sawtooth-style pulse]]
+
+
+
+--time
+Tutorials["time"] = [[time() gives you world time, it is RealTime(). It's the server time unaffected by sv_timescale or other factors like pausing.]]
+Tutorials["synced_time"] = [[synced_time() gives you CurTime(). It's the server time affected by sv_timescale and other factors like pausing.]]
+Tutorials["systime"] = [[systime() gives you system time, it is SysTime()]] Tutorials["stime"] = Tutorials["systime"]
+Tutorials["frametime"] = [[frametime() gives you FrameTime(), which is how long it took to draw the previous frame on your screen.
+It is used when adjusting iteratively by addition while maintaining a steady speed]] Tutorials["ftime"] = Tutorials["frametime"]
+Tutorials["framenumber"] = [["framenumber() gives you FrameNumber(), which is the amount of frames that were drawn since you loaded the map"]] Tutorials["fnumber"] = Tutorials["framenumber"]
+Tutorials["timeex"] = [[timeex() is the time in seconds counted from when the proxy was shown, like a chronometer / stopwatch. It is a simple yet capital building block in an immense amount of the proxies you can think of.]]
+
+--randomness and interpolation
+Tutorials["ezfade"] = [[ezfade(speed, starttime, endtime) creates standard fades flexibly. think of clamps. this is that, but a bit easier and quicker to set up.
+
+clamp(timeex(), 0, 1) = ezfade() = ezfade(1)
+clamp(1 - timeex(), 0, 1) = ezfade(-1)
+clamp(-9 + 2*timeex(), 0, 1) = ezfade(2, 4.5)
+clamp(-1 + 0.5*timeex(),0 , 1)*clamp(2 - timeex()*0.5, 0, 1) = ezfade(0.5, 2, 6)
+
+speed (optional, default = 1) is how fast the fades should work. the speed is the same for the fade-in and the fade-out. 2 means it will last half a second. negative values gives you the simple fadeout setup
+starttime (optional, default = 0) is when the fade should start happening, starting from showtime. if speed is negative, starttime will count as an endtime
+endtime (optional) is when the fade-out should end.
+
+keep in mind we are still working in normalized space, we're within 0 and 1. it is suitable for float-based variables like Alpha.
+but if you want to use that in another variable you might have to adjust by multiplying.
+
+it's by design. forget about putting your min and max inside the function. work in normalized terms to have a clear view of the time.]]
+
+Tutorials["ezfade_4pt"] = [[ezfade_4pt(in_starttime, in_endtime, out_starttime, out_endtime) creates a fadein-fadeout setup in normalized ranges i.e. [0,1] based on four time points rather than speeds
+
+in_starttime is the time when the fadein should start (where it starts moving from 0 to 1).
+in_endtime is the time when the fadein should end (where it is 1)
+out_starttime is the time when the fadeout should start (where it starts moving from 1 back to 0), it is optional if you wish to have only a fadein
+out_endtime is the time when the fadeout should end (where it finally stops at 0)]]
+Tutorials["random"] = [[random(min, max) gives you a random number every time it is called. it will flicker like mad.
+
+min and max are optional arguments. they are implicitly 0 and 1 if not specified.
+
+for held randomness, please review random_once(seed, min, max) and sample_and_hold(seed, duration, min, max, ease)]]
+Tutorials["random_once"] = [[random_once(seed, min, max) gives you a random number every time the proxy is shown. it will reset when the proxy is hidden and shown.
+
+seed is an optional argument, it is implicitly 0 if not specified. seed will allow you to choose between independent or shared sources. they are not a true RNG seed (you still get new randomness every showtime) but they work similarly.
+min and max are optional arguments. they are implicitly 0 and 1 if not specified.
+
+e.g. you might have random_once(1),random_once(2),random_once(2) resulting in 0.141, 0.684, 0.684
+
+idea: you can use sgn to randomly pick a side with sgn(random_once(0,-1,1))
+
+also see sample_and_hold(seed, duration, min, max, ease)]]
+Tutorials["lerp"] = [[lerp(fraction, min, max) interpolates linearly between min and max, by the fraction provided. It is ike an adjustable middle point
+
+fraction is how far in the interpolation we are at. it is implicitly 0 if not provided
+min is the start or minimum, it is optional, implicitly -1 if not specified
+max is the end or maximum, it is optional, implicitly 1 if not specified
+
+the formula is (max - min) * frac + min
+
+e.g. you might have lerp(ezfade(),0,10) which would move from 0 to 10 in 1 second]]
+
+Tutorials["ease"] = [[eases are interpolations, but more special.
+eases are fun.
+
+eases have several variations.
+a typical ease is easeInSine(fraction, min, max)
+it would be the same as ease_InSine(fraction, min, max) and InSine(fraction, min, max)
+they interpolate between min and max, by the fraction provided, but with more dynamic curves.
+
+fraction is how far in the interpolation we are at.
+min is the start or minimum, it is optional, implicitly 0 if not specified
+max is the end or maximum, it is optional, implicitly 1 if not specified
+
+ease "flavors": Sine, Quad, Cubic, Quart, Circ, Expo, Sine, Back, Bounce, Elastic
+For every ease "flavor", there is an "In", an "Out" and an "InOut" version.
+you'll have eases written like easeOutBack, ease_InOutSine, InCirc etc.
+
+here's a quick tip. use ezfade to easily get a transition going. you can even multiply outside instead of putting your min and max inside the function.
+e.g.20*easeInSine(ezfade())]]
+
+for ease,f in pairs(math.ease) do
+ if string.find(ease,"In") or string.find(ease,"Out") then
+ Tutorials[ease] = Tutorials["ease"]
+ Tutorials["ease_"..ease] = Tutorials["ease"]
+ Tutorials["ease"..ease] = Tutorials["ease"]
+ end
+end
+
+Tutorials["sample_and_hold"] = [[sample_and_hold(seed, duration, min, max, ease) or samplehold or drift or random_drift
+
+it's a type of regularly-refreshing random emitter with extra steps.
+
+seed is like a RNG seed so you can decide whether you want a shared source or independent ones.
+duration is how often in seconds the value should move to a new random value. it is 1 if not specified.
+min and max are self-explanatory
+ease is a string corresponding to the ease name. we expect quotes like "InSine" or "linear". without that argument, it is a sample and hold (no easing)
+
+reminder there are many variations of eases. here are some examples showing some flavors, the In\Out types and the different alternative ways of writing them.
+"easeInSine", "ease_OutBack", "InOutQuad"
+the full list of ease "flavors": Sine, Quad, Cubic, Quart, Circ, Expo, Sine, Back, Bounce, Elastic]]
+Tutorials["samplehold"] = Tutorials["sample_and_hold"]
+Tutorials["drift"] = Tutorials["sample_and_hold"]
+Tutorials["random_drift"] = Tutorials["sample_and_hold"]
+
+
+--voice
+Tutorials["voice_volume"] = [[voice_volume() reads your voice volume, it has ranges of [0,1], but 1 is an absurdly high volume. please don't scream into the mic.]]
+Tutorials["voice_volume_scale"] = [[voice_volume_scale() reads your voice volume scale setting which affects how much volume you transmit, it has ranges of [0,1].]]
+
+
+--statistics
+Tutorials["sum"] = [[sum(...) adds all the arguments]]
+Tutorials["product"] = [[product(...) takes the product of the arguments]]
+Tutorials["average"] = [[average(...) or mean takes the average of the arguments
+it is the sum of the arguments divided by the number of arguments]]
+Tutorials["mean"] = Tutorials["average"]
+Tutorials["median"] = [[median(...) takes the median of the arguments
+
+it is like the middle element in a list when sorted
+if there are an even number of arguments, the median is the average of the two middle elements]]
+Tutorials["event_alternative"] = [[event_alternative(uid1, num1, num2) or if_event or if_else_event finds out whether an event is active, and returns num1 or num2 depending on whether it's on or off
+
+uid1 is a string (text), we expect quotes like "w_button", you have to get their uid or name to identify the parts. please avoid having multiple parts bearing the same name if that's the case.
+full UID can be copied from the part in the copy menu
+
+num1 is the value to return if the event is acive (hiding parts), it is optional, 0 by default.
+num2 is the value to return if the event is inactive (showing parts), it is optional, 1 by default
+
+those default values are useful for boolean (true/false) variables if you want to link them to an event. they will reflect the state of the event without any fuss]]
+Tutorials["if_event"] = Tutorials["event_alternative"]
+Tutorials["if_else_event"] = Tutorials["event_alternative"]
+
+
+--aim, eye position, visibility
+Tutorials["owner_fov"] = [[owner_fov() gets your field of view in degrees]]
+Tutorials["visible"] =
+[[visible(radius) gives you whether the physical target is visible or not.
+
+radius is an optional argument. it is implicitly 16 if not specified.
+
+the physical target is usually the parent model, the visibility is considered if a radius circle would be "pixel-visible".
+it will give 1 if visible, 0 if not visible. being non-visible happens with world objects, being outside of FOV and with pac drawables]]
+Tutorials["eye_position_distance"] = [[eye_position_distance() takes the distance from the part's physical target (target or parent) to the viewer eye position
+
+it is not very suitable for fading camera effects based on distance, since if you put something in front of the eyes, it will be at near-zero distance
+
+see also part_distance(uid1,uid2)]]
+
+Tutorials["eye_angle_distance"] = [[eye_angle_distance() tells you how much of an "angle" you have, from the viewer eye angle to the line from the physical target (target or parent) eye position
+
+it's using a normalized vector dot products for this
+
+usually you'll have 0.5 if the viewer is looking straight at the target, down to 0 if looking away at 45 degrees or so]]
+
+Tutorials["aim_length"] = [[aim_length() takes the distance to the traced aimed point. It's how far you look.]]
+Tutorials["aim_length_fraction"] = [[aim_length_fraction() takes the fractional distance to the traced aimed point. It's how far you look, but as a proportion of 16000.]]
+
+Tutorials["flat_dot_forward"] = [[flat_dot_forward() takes the dot product of the yaw of the owner/part's angles, against the forward angle from the viewer to the owner/part.
+
+to break it down, it just means to compare how the subject is oriented relative to the viewer.
+
+-1 is facing away from the viewer
+0 is right angled orientation (left/right)
+1 is facing toward the viewer
+
+a similar idea is used in the south park example pac for picking different 2D sprites based on where we're looking]]
+
+Tutorials["flat_dot_right"] = [[flat_dot_right() takes the dot product of the yaw of the owner/part's angles, against the right angle from the viewer to the owner/part.
+
+to break it down, it just means to compare how the subject is oriented relative to the viewer.
+
+0 is facing away or toward the viewer
+-1 is when the subject is facing left
+1 is when the subject is facing right
+
+a similar idea is used in the south park example pac for picking different 2D sprites based on where we're looking]]
+
+Tutorials["owner_eye_angle_pitch"] = [[owner_eye_angle_pitch() takes the upward eye angle
+the ranges are about [0,1].
+you'll usually have root owner checked for this.]]
+Tutorials["owner_eye_angle_yaw"] = [[owner_eye_angle_yaw() takes the sideways eye angle
+the ranges are about [-2,2].
+you'll usually have root owner checked for this.]]
+Tutorials["owner_eye_angle_roll"] = [[owner_eye_angle_roll() takes the tilt of eye angle.
+you'll usually have root owner checked for this.
+you normally won't have a roll in your eye angles.]]
+
+
+--position, velocity, vectors
+Tutorials["part_distance"] = [[part_distance(uid1, uid2) takes the distance between two base_movable parts like models
+
+uid1 and uid2 are strings (text), we expect quotes like "center_pos", you have to get their uid or name to identify the parts. please avoid having multiple parts bearing the same name if that's the case.
+full UID can be copied from the part in the copy menu
+
+uid2 is optional, it is implicitly using the proxy's parent model for example.
+that's useful if you have something spawned from a projectile, because projectile creates new parts with new uids, which meant the set uid would match the old base part otherwise]]
+
+Tutorials["Vector"] = [[Vector(x,y,z) creates a vector. It has access to vector functions like Vector(0,0,1):Dot(Vector(2,0,0))]]
+
+Tutorials["owner_position"] = [[owner_position() gets the owner's world position.
+the owner is either the parent model or the owning entity i.e. your player]]
+Tutorials["owner_position_x"] = [[owner_position_x() gets the owner's world position on x.
+the owner is either the parent model or the owning entity i.e. your player]]
+Tutorials["owner_position_y"] = [[owner_position_y() gets the owner's world position on y.
+the owner is either the parent model or the owning entity i.e. your player]]
+Tutorials["owner_position_z"] = [[owner_position_z() gets the owner's world position on z. the owner is either the parent model or the owning entity i.e. your player]]
+
+Tutorials["part_pos"] = [[part_pos(uid1) takes the position of a base_movable part like models
+
+uid1 is a string (text), we expect quotes like "center_pos", you have to get their uid or name to identify the parts. please avoid having multiple parts bearing the same name if that's the case.
+full UID can be copied from the part in the copy menu
+
+uid1 is optional, it is implicitly using the proxy's parent model for example]]
+Tutorials["part_pos_x"] = [[part_pos_x(uid1) takes the X (perhaps north/south) world position of a base_movable part like models
+
+uid1 is a string (text), we expect quotes like "center_pos", you have to get their uid or name to identify the parts. please avoid having multiple parts bearing the same name if that's the case.
+full UID can be copied from the part in the copy menu
+
+uid1 is optional, it is implicitly using the proxy's parent model for example]]
+
+Tutorials["part_pos_y"] = [[part_pos_y(uid1) takes the Y (perhaps east/west) world position of a base_movable part like models
+
+uid1 is a string (text), we expect quotes like "center_pos", you have to get their uid or name to identify the parts. please avoid having multiple parts bearing the same name if that's the case.
+full UID can be copied from the part in the copy menu
+
+uid1 is optional, it is implicitly using the proxy's parent model for example]]
+
+Tutorials["part_pos_z"] = [[part_pos_z(uid1) takes the Z (up/down) world position of a base_movable part like models
+
+uid1 is a string (text), we expect quotes like "center_pos", you have to get their uid or name to identify the parts. please avoid having multiple parts bearing the same name if that's the case.
+full UID can be copied from the part in the copy menu
+
+uid1 is optional, it is implicitly using the proxy's parent model for example]]
+
+Tutorials["delta_pos"] = [[delta_pos(uid1, uid2) takes the difference of world positions as a vector, between two base_movable parts like models.
+mind the order. it is doing (pos2 - pos1) like a standard delta
+
+uid1 and uid2 are strings (text), we expect quotes like "center_pos", you have to get their uid or name to identify the parts. please avoid having multiple parts bearing the same name if that's the case.
+full UID can be copied from the part in the copy menu
+
+uid2 is optional, it is implicitly using the proxy's parent model for example.
+that's useful if you have something spawned from a projectile, because projectile creates new parts with new uids, which means the set uid would match the old part otherwise]]
+
+Tutorials["delta_x"] = [[delta_x(uid1, uid2) takes the difference of X (perhaps north/south) world coordinates, between two base_movable parts like models
+mind the order. it is doing (pos2.x - pos1.x) like a standard delta
+
+uid1 and uid2 are strings (text), we expect quotes like "center_pos", you have to get their uid or name to identify the parts. please avoid having multiple parts bearing the same name if that's the case.
+full UID can be copied from the part in the copy menu
+
+uid2 is optional, it is implicitly using the proxy's parent model for example.
+that's useful if you have something spawned from a projectile, because projectile creates new parts with new uids, which means the set uid would match the old part otherwise]]
+
+Tutorials["delta_y"] = [[delta_y(uid1, uid2) takes the difference of Y (perhaps east/west) world coordinates, between two base_movable parts like models
+mind the order. it is doing (pos2.y - pos1.y) like a standard delta
+
+uid1 and uid2 are strings (text), we expect quotes like "center_pos", you have to get their uid or name to identify the parts. please avoid having multiple parts bearing the same name if that's the case.
+full UID can be copied from the part in the copy menu
+
+uid2 is optional, it is implicitly using the proxy's parent model for example.
+that's useful if you have something spawned from a projectile, because projectile creates new parts with new uids, which means the set uid would match the old part otherwise]]
+
+Tutorials["delta_z"] = [[delta_z(uid1, uid2) takes the difference of Z world coordinates (height), between two base_movable parts like models
+mind the order. it is doing (pos2.z - pos1.z) like a standard delta
+
+uid1 and uid2 are strings (text), we expect quotes like "center_pos", you have to get their uid or name to identify the parts. please avoid having multiple parts bearing the same name if that's the case.
+full UID can be copied from the part in the copy menu
+
+uid2 is optional, it is implicitly using the proxy's parent model for example.
+that's useful if you have something spawned from a projectile, because projectile creates new parts with new uids, which means the set uid would match the old part otherwise]]
+
+Tutorials["owner_velocity_length_increase"] = [[owner_velocity_length_increase() takes overall speed and takes it to gradually increase in value
+it builds up, making it good for wheels and such, although the velocity is taken at its length so it won't go backwards. see owner_velocity_forward_increase.]]
+
+Tutorials["owner_velocity_forward_increase"] = [[owner_velocity_forward_increase() takes forward speed (velocity dotted with eye angles) and takes it to gradually increase or decrease in value
+it builds up, making it good for wheels and such. although the wheels' angles would constantly change, so it might be weird with the dot product
+so maybe you should use an invalidbone model as a source for the "mileage" variable and make it an extra expression updated at invalidbone and read on the wheels]]
+Tutorials["owner_velocity_right_increase"] = [[owner_velocity_right_increase() takes right speed (velocity dotted with eye angles) and takes it to gradually increase or decrease in value
+it builds up, making it good for wheels and such. although the wheels' angles would constantly change, so it might be weird with the dot product
+so maybe you should use an invalidbone model as a source for the "mileage" variable and make it an extra expression updated at invalidbone and read on the wheels]]
+Tutorials["owner_velocity_up_increase"] = [[owner_velocity_up_increase() takes up speed (velocity dotted with eye angles) and takes it to gradually increase or decrease in value
+it builds up, making it good for wheels and such. although the wheels' angles would constantly change, so it might be weird with the dot product
+so maybe you should use an invalidbone model as a source for the "mileage" variable and make it an extra expression updated at invalidbone and read on the wheels]]
+
+Tutorials["owner_velocity_world_forward_increase"] = [[owner_velocity_world_forward_increase() takes X speed (world coordinates) and takes it to gradually increase or decrease in value
+it builds up, making it good for wheels and such. although we're in global coordinates so maybe not.]]
+Tutorials["owner_velocity_world_right_increase"] = [[owner_velocity_world_right_increase() takes Y speed (world coordinates) and takes it to gradually increase or decrease in value
+it builds up, making it good for wheels and such. although we're in global coordinates so maybe not.]]
+Tutorials["owner_velocity_world_up_increase"] = [[owner_velocity_world_up_increase() takes X speed (world coordinates) and takes it to gradually increase or decrease in value.]]
+
+Tutorials["parent_velocity_length"] = [[parent_velocity_length() takes the physical target (target part or parent) overall speed]]
+Tutorials["parent_velocity_forward"] = [[parent_velocity_forward() takes the physical target (target part or parent) velocity dotted against the forward of its angle]]
+Tutorials["parent_velocity_right"] = [[parent_velocity_right() takes the physical target (target part or parent) velocity dotted against the right of its angle]]
+Tutorials["parent_velocity_up"] = [[parent_velocity_up() takes the physical target (target part or parent) velocity dotted against the up of its angle]]
+
+Tutorials["owner_velocity_length"] = [[owner_velocity_length() takes owner's overall speed.
+normal running will usually be 4.5, sprinting is 9, crouching is 1.3, walking is 2.2
+
+this function uses the velocity roughness and reset velocities on hide from the behavior section
+more roughness means less frame-by-frame smoothing and more direct readouts, although these readouts will be unreliable if the FPS varies.
+reset velocities on hide clears the smoothing memory]]
+Tutorials["owner_velocity_forward"] = [[owner_velocity_forward() takes owner's forward speed compared to the eye angles.
+it's made "forward" by doing a dot product with the eye angles. it will be reduced if you look up or down.
+if you want to ignore eye angles, you could set it up on a model part located on invalidbone while not using root owner, and use an extra expression to outsource the result elsewhere
+
+normal running will usually be -4.5, sprinting is -9, crouching is -1.3, walking is -2.2. going back will make these positive.
+
+this function uses the velocity roughness and reset velocities on hide from the behavior section
+more roughness means less frame-by-frame smoothing and more direct readouts, although these readouts will be unreliable if the FPS varies.
+reset velocities on hide clears the smoothing memory]]
+Tutorials["owner_velocity_right"] = [[owner_velocity_right() takes owner's right speed compared to the eye angles.
+it's made "right" by doing a dot product with the eye angles. it will be reduced if you look away, but normally it shouldn't happen if you're actively moving. it can happen if you fling yourself with noclip and look around.
+if you want to ignore eye angles, you could set it up on a model part located on invalidbone while not using root owner, and use an extra expression to outsource the result elsewhere
+
+normal running will usually be -4.5, sprinting is -9, crouching is -1.3, walking is -2.2. going left will make these positive.
+
+this function uses the velocity roughness and reset velocities on hide from the behavior section
+more roughness means less frame-by-frame smoothing and more direct readouts, although these readouts will be unreliable if the FPS varies.
+reset velocities on hide clears the smoothing memory]]
+Tutorials["owner_velocity_up"] = [[owner_velocity_up() takes owner's up speed compared to the eye angles.
+it's made "up" by doing a dot product with the eye angles. it will be reduced if you look up or down.
+if you want to ignore eye angles, you could set it up on a model part located on invalidbone while not using root owner, and use an extra expression to outsource the result elsewhere
+
+normal noclipping going up will usually be -12, falling at terminal velocity will usually be 20.
+
+this function uses the velocity roughness and reset velocities on hide from the behavior section
+more roughness means less frame-by-frame smoothing and more direct readouts, although these readouts will be unreliable if the FPS varies.
+reset velocities on hide clears the smoothing memory]]
+
+Tutorials["owner_velocity_world_forward"] = [[owner_velocity_world_forward() takes owner's X (north/south?) speed in terms of world (global) coordinates
+not that it matters, but going to +X, normal running will usually be 4.5, sprinting is 9, crouching is 1.3, walking is 2.2.]]
+Tutorials["owner_velocity_world_right"] = [[owner_velocity_world_right() takes owner's Y (east/west?) speed in terms of world (global) coordinates
+not that it matters, but going to +Y, normal running will usually be 4.5, sprinting is 9, crouching is 1.3, walking is 2.2.]]
+Tutorials["owner_velocity_world_up"] = [[owner_velocity_world_up() takes owner's Z (up/down) speed in terms of world (global) coordinates
+normal noclipping going up will usually be -12, falling at terminal velocity will usually be 20.]]
+
+
+--model parameters
+Tutorials["pose_parameter"] = [[pose_parameter(name) takes the value of the owner's pose parameter. it can be in weird ranges.
+
+name is the name of the pose parameter to read. it's a string, we expect quotes e.g. pose_parameter("head_pitch")
+
+keep in mind most non-biped and most models not made as playermodels do not have the usual pose parameters.
+if you're using a monster-type model as a PM, stay in your usual humanoid PM.
+make the monster a model on invalidbone and use use root owner for your pose parameters
+
+also see pose_parameter_true for an alternative adjusted output that more accurately reflects the value of the pose parameter.
+e.g. while pose_parameter("head_yaw") might range from [0.2,0.8], pose_parameter_true("head_yaw") would range from [-45,45].
+since most people want a symmetrical thing, they'd need a 45*(-1 + 2*pose_parameter("head_yaw")) style setup
+I think it's more convenient to use pose_parameter_true("head_yaw")]]
+
+Tutorials["pose_parameter_true"] = [[pose_parameter_true(name) takes the value of the owner's pose parameter adjusted to get its "true value".
+
+name is the name of the pose parameter to read. it's a string, we expect quotes e.g. pose_parameter("head_pitch")
+
+keep in mind most non-biped and most models not made as playermodels do not have the usual pose parameters.
+if you're using a monster-type model as a PM, stay in your usual humanoid PM.
+make the monster a model on invalidbone and use use root owner for your pose parameters
+
+e.g. while pose_parameter("head_yaw") might range from [0.2,0.8], pose_parameter_true("head_yaw") would range from [-45,45].
+since most people want a symmetrical thing, they'd need a 45*(-1 + 2*pose_parameter("head_yaw")) style setup
+I think it's more convenient to use pose_parameter_true("head_yaw")]]
+
+Tutorials["bodygroup"] = [[bodygroup(name, uid) or model_bodygroup reads the parent or the referenced part's bodygroup.
+
+name is the name of the bodygroup, it's a string, we expect quotes.
+uid is the Unique ID or name of a part, it's a string, we expect quotes again]]
+Tutorials["model_bodygroup"] = Tutorials["bodygroup"]
+
+Tutorials["parent_scale_x"] = [[parent_scale_x() takes the X scale (with size) of the physical target (target part or parent)]]
+Tutorials["parent_scale_y"] = [[parent_scale_y() takes the Y scale (with size) of the physical target (target part or parent)]]
+Tutorials["parent_scale_z"] = [[parent_scale_z() takes the Z scale (with size) of the physical target (target part or parent)]]
+
+Tutorials["owner_scale_x"] = [[owner_scale_x() takes owner's model scale on x.
+it combines the Size and Scale from pac. if owner.pac_model_scale does not exist, it may use owner.GetModelScale
+e.g. with a size of 2 and a scale of (3,2,1), it will be 6.]]
+Tutorials["owner_scale_y"] = [[owner_scale_y() takes owner's model scale on y.
+it combines the Size and Scale from pac. if owner.pac_model_scale does not exist, it may use owner.GetModelScale
+e.g. with a size of 2 and a scale of (3,2,1), it will be 4.]]
+Tutorials["owner_scale_z"] = [[owner_scale_z() takes owner's model scale on z.
+it combines the Size and Scale from pac. if owner.pac_model_scale does not exist, it may use owner.GetModelScale
+e.g. with a size of 2 and a scale of (3,2,1), it will be 2.]]
+
+
+--lighting and color
+Tutorials["light_amount"] = [[light_amount() reads the physical target (target part or parent) nearby lighting as a color-vector. components are in ranges of [0,1].]]
+Tutorials["light_amount_r"] = [[light_amount_r() reads the physical target (target part or parent) nearby lighting's red component, ranges are [0,1].]]
+Tutorials["light_amount_g"] = [[light_amount_g() reads the physical target (target part or parent) nearby lighting's green component, ranges are [0,1].]]
+Tutorials["light_amount_b"] = [[light_amount_b() reads the physical target (target part or parent) nearby lighting's blue component, ranges are [0,1].]]
+Tutorials["light_value"] = [[light_value() reads the physical target (target part or parent) nearby lighting and takes its value (brightness), ranges are [0,1].]]
+Tutorials["ambient_light"] = [[ambient_light() reads the global ambient lighting as a color-vector (255,255,255). it may adjust to Proper Color ranges (1,1,1) depending on the part.]]
+Tutorials["ambient_light_r"] = [[ambient_light_r() reads the global ambient lighting's red component. it may adjust to Proper Color ranges (1,1,1) depending on the part.]]
+Tutorials["ambient_light_g"] = [[ambient_light_g() reads the global ambient lighting's green component. it may adjust to Proper Color ranges (1,1,1) depending on the part.]]
+Tutorials["ambient_light_b"] = [[ambient_light_b() reads the global ambient lighting's blue component. it may adjust to Proper Color ranges (1,1,1) depending on the part.]]
+Tutorials["hsv_to_color"] = [[hsv_to_color(h,s,v) reads a hue, saturation and value and expands it into an RGB color-vector.
+
+h is the hue, it's the color in terms of an angle, ranging from 0 to 360, it will take the remainder of 360 to loop back.
+0 = red
+30 = orange
+60 = yellow
+90 = lime green
+120 = green
+150 = teal
+180 = cyan
+210 = light blue
+240 = dark blue
+270 = purple
+300 = magenta
+330 = magenta-red
+360 = red
+
+s is the saturation. at 0 it is white. at more than 1 it distorts the color
+
+v is the value, the brightness. at 0 it is black. at more than 1 it distorts the color]]
+
+--health and armor
+Tutorials["owner_health"] = [[owner_health() reads your current player health.]]
+Tutorials["owner_max_health"] = [[owner_max_health() reads your maximum player health.]]
+Tutorials["owner_health_fraction"] = [[owner_health_fraction() reads your health as a fraction between your current health and maximum health. 50 of 200 is 0.25, 100 of 100 is 1]]
+Tutorials["owner_armor"] = [[owner_armor() reads your current player HEV suit armor.]]
+Tutorials["owner_max_armor"] = [[owner_max_armor() reads your maximum player HEV suit armor.]]
+Tutorials["owner_armor_fraction"] = [[owner_armor_fraction() reads your HEV suit armor as a fraction between your current armor and maximum armor. 50 of 200 is 0.25, 100 of 100 is 1]]
+
+
+--entity colors
+Tutorials["player_color"] = [[player_color() reads the player color as a color-vector (255,255,255). it may adjust to Proper Color ranges (1,1,1) depending on the part.]]
+Tutorials["player_color_r"] = [[player_color_r() reads the player color's red component. it may adjust to Proper Color ranges (1,1,1) depending on the part.]]
+Tutorials["player_color_g"] = [[player_color_g() reads the player color's green component. it may adjust to Proper Color ranges (1,1,1) depending on the part.]]
+Tutorials["player_color_b"] = [[player_color_b() reads the player color's blue component. it may adjust to Proper Color ranges (1,1,1) depending on the part.]]
+Tutorials["weapon_color"] = [[weapon_color() reads the weapon color as a color-vector (255,255,255). it may adjust to Proper Color ranges (1,1,1) depending on the part.]]
+Tutorials["weapon_color_r"] = [[weapon_color_r() reads the weapon color's red component. it may adjust to Proper Color ranges (1,1,1) depending on the part.]]
+Tutorials["weapon_color_g"] = [[weapon_color_g() reads the weapon color's green component. it may adjust to Proper Color ranges (1,1,1) depending on the part.]]
+Tutorials["weapon_color_b"] = [[weapon_color_b() reads the weapon color's blue component. it may adjust to Proper Color ranges (1,1,1) depending on the part.]]
+Tutorials["ent_color"] = [[ent_color() reads the entity (root owner (true entity) or parent/target part (pac entity)) color as a color-vector (255,255,255). it may adjust to Proper Color ranges (1,1,1) depending on the part.]]
+Tutorials["ent_color_r"] = [[ent_color_r() reads the entity (root owner (true entity) or parent/target part (pac entity)) color's red component. it may adjust to Proper Color ranges (1,1,1) depending on the part.]]
+Tutorials["ent_color_g"] = [[ent_color_g() reads the entity (root owner (true entity) or parent/target part (pac entity)) color's green component. it may adjust to Proper Color ranges (1,1,1) depending on the part.]]
+Tutorials["ent_color_b"] = [[ent_color_b() reads the entity (root owner (true entity) or parent/target part (pac entity)) color's blue component. it may adjust to Proper Color ranges (1,1,1) depending on the part.]]
+Tutorials["ent_color_a"] = [[ent_color_b() reads the entity (root owner (true entity) or parent/target part (pac entity)) color's alpha component. it may adjust to Proper Color ranges (1,1,1) depending on the part.]]
+
+
+--ammo
+Tutorials["owner_total_ammo"] = [[owner_total_ammo(id) reads an ammo type's current ammo reserves.
+id is a string for the ammo name. it's a string, we expect quotes. it is a name like "Pistol", "357", "SMG1", "SMG1_Grenade", "AR2", "AR2AltFire", etc.]]
+Tutorials["weapon_primary_ammo"] = [[weapon_primary_ammo() reads your current clip's primary ammo on your active weapon.]]
+Tutorials["weapon_primary_total_ammo"] = [[weapon_primary_total_ammo() reads your current primary ammo reserves on your active weapon.]]
+Tutorials["weapon_primary_clipsize"] = [[weapon_primary_clipsize() reads your primary clip size on your active weapon.]]
+Tutorials["weapon_secondary_ammo"] = [[weapon_secondary_ammo() reads your current clip's secondary ammo on your active weapon.]]
+Tutorials["weapon_secondary_total_ammo"] = [[weapon_secondary_total_ammo() reads your current secondary ammo reserves on your active weapon.]]
+Tutorials["weapon_secondary_clipsize"] = [[weapon_secondary_clipsize() reads your secondary clip size on your active weapon.]]
+
+
+--server population
+Tutorials["server_maxplayers"] = [[server_maxplayers() gets the server capacity.]]
+Tutorials["server_playercount"] = [[server_playercount() or server_population, gets the server population (number of players).]]
+Tutorials["server_population"] = Tutorials["server_playercount"]
+Tutorials["server_botcount"] = [[server_botcount() gets the number of bot players connected to the server.]]
+Tutorials["server_humancount"] = [[server_botcount() gets the number of human players connected to the server.]]
+
+
+--health modifier extra health bars
+Tutorials["pac_healthbars_total"] = [[pac_healthbars_total() or healthmod_bar_total gets the total amount of "extra health" granted by your health modifiers.]]
+Tutorials["healthmod_bar_total"] = Tutorials["pac_healthbars_total"]
+Tutorials["pac_healthbars_layertotal"] = [[pac_healthbars_layertotal(layer) or healthmod_bar_layertotal gets the total amount of "extra health" granted by your health modifiers on a certain layer.
+layer should be a number, they are usually whole numbers from 0 to 15, with bigger numbers being damaged first]]
+Tutorials["healthmod_bar_layertotal"] = Tutorials["healthmod_bar_layertotal"]
+Tutorials["pac_healthbar_uidvalue"] = [[pac_healthbar_uidvalue(uid) or healthmod_bar_uidvalue gets the amount of "extra health" granted by one of your health modifier parts.
+uid is a string corresponding to the name or Unique ID of the part, we expect quotes.]]
+Tutorials["healthmod_bar_uidvalue"] = Tutorials["pac_healthbar_uidvalue"]
+Tutorials["pac_healthbar_remaining_bars"] = [[healthmod_bar_remaining_bars(uid) or pac_healthbar_remaining_bars gets the remaining number of "extra health" bars granted by one of your health modifier parts.
+uid is a string corresponding to the name or Unique ID of the part, we expect quotes.]]
+Tutorials["healthmod_bar_remaining_bars"] = Tutorials["pac_healthbar_remaining_bars"]
+
+return Tutorials
diff --git a/lua/pac3/editor/client/saved_parts.lua b/lua/pac3/editor/client/saved_parts.lua
index 6bee23138..4ef455512 100644
--- a/lua/pac3/editor/client/saved_parts.lua
+++ b/lua/pac3/editor/client/saved_parts.lua
@@ -1,10 +1,11 @@
local L = pace.LanguageString
-- load only when hovered above
-local function add_expensive_submenu_load(pnl, callback)
+local function add_expensive_submenu_load(pnl, callback, subdir)
+
local old = pnl.OnCursorEntered
pnl.OnCursorEntered = function(...)
- callback()
+ callback(subdir)
pnl.OnCursorEntered = old
return old(...)
end
@@ -57,7 +58,7 @@ function pace.SaveParts(name, prompt_name, override_part, overrideAsUsual)
end
end
- data = hook.Run("pac_pace.SaveParts", data) or data
+ data = pac.CallHook("pace.SaveParts", data) or data
if not override_part and #file.Find("pac3/sessions/*", "DATA") > 0 and not name:find("/") then
pace.luadata.WriteFile("pac3/sessions/" .. name .. ".txt", data)
@@ -95,6 +96,10 @@ end
local last_backup
local maxBackups = CreateConVar("pac_backup_limit", "100", {FCVAR_ARCHIVE}, "Maximal amount of backups")
+local autoload_prompt = CreateConVar("pac_prompt_for_autoload", "1", {FCVAR_ARCHIVE}, "Whether to ask before loading autoload. The prompt can let you choose to not load, pick autoload or the newest backup")
+local auto_spawn_prop = CreateConVar("pac_autoload_preferred_prop", "2", {FCVAR_ARCHIVE}, "When loading a pac with an owner name suggesting a prop, notify you and then wait before auto-applying the outfit next time you spawn a prop.\n" ..
+ "0 : do not check\n1 : check if only 1 such group is present\n2 : check if multiple such groups are present and queue one group at a time")
+
function pace.Backup(data, name)
name = name or ""
@@ -132,13 +137,34 @@ function pace.Backup(data, name)
local str = pace.luadata.Encode(data)
if str ~= last_backup then
- file.Write("pac3/__backup/" .. (name=="" and name or (name..'_')) .. date .. ".txt", str)
+ file.Write("pac3/__backup/" .. (name == "" and name or (name .. "_")) .. date .. ".txt", str)
last_backup = str
end
end
end
+local latestprop
+local latest_uid
+if game.SinglePlayer() then
+ pac.AddHook("OnEntityCreated", "queue_proppacs", function( ent )
+ if ( ent:GetClass() == "prop_physics" or ent:IsNPC()) and not ent:CreatedByMap() and LocalPlayer().pac_propload_queuedparts then
+ if not table.IsEmpty(LocalPlayer().pac_propload_queuedparts) then
+ ent:EmitSound( "buttons/button4.wav" )
+ local root = LocalPlayer().pac_propload_queuedparts[next(LocalPlayer().pac_propload_queuedparts)]
+ root.self.OwnerName = ent:EntIndex()
+ latest_uid = root.self.UniqueID
+ pace.LoadPartsFromTable(root, false, false)
+ LocalPlayer().pac_propload_queuedparts[next(LocalPlayer().pac_propload_queuedparts)] = nil
+ latestprop = ent
+ end
+
+ end
+ end)
+end
+
+
function pace.LoadParts(name, clear, override_part)
+
if not name then
local frm = vgui.Create("DFrame")
frm:SetTitle(L"parts")
@@ -175,6 +201,11 @@ function pace.LoadParts(name, clear, override_part)
end
else
+ if name ~= "autoload.txt" and not string.find(name, "pac3/__backup") then
+ if file.Exists("pac3/" .. name .. ".txt", "DATA") then
+ cookie.Set( "pac_last_loaded_outfit", name .. ".txt" )
+ end
+ end
if hook.Run("PrePACLoadOutfit", name) == false then
return
end
@@ -184,13 +215,13 @@ function pace.LoadParts(name, clear, override_part)
if name:find("https?://") then
local function callback(str)
if string.find( str, "" ) then
- pace.MessagePrompt("Invalid URL, the website returned a HTML file. If you're using Github then use the RAW option.", "URL Failed", "OK")
+ pace.MessagePrompt("Invalid URL, .txt expected, but the website returned a HTML file. If you're using Github then use the RAW option.", "URL Failed", "OK")
return
end
local data, err = pace.luadata.Decode(str)
if not data then
- local message = string.format("URL fail: %s : %s\n", name, err)
+ local message = string.format("Failed to load pac3 outfit from url: %s : %s\n", name, err)
pace.MessagePrompt(message, "URL Failed", "OK")
return
end
@@ -204,32 +235,69 @@ function pace.LoadParts(name, clear, override_part)
else
name = name:gsub("%.txt", "")
- local data,err = pace.luadata.ReadFile("pac3/" .. name .. ".txt")
+ local data, err = pace.luadata.ReadFile("pac3/" .. name .. ".txt")
+ local has_possible_prop_pacs = false
- if name == "autoload" and (not data or not next(data)) then
- local err
- data,err = pace.luadata.ReadFile("pac3/sessions/" .. name .. ".txt",nil,true)
- if not data then
- if err then
- ErrorNoHalt(("Autoload failed: %s\n"):format(err))
+ if data and istable(data) then
+ for i, part in pairs(data) do
+ if part.self and isnumber(tonumber(part.self.OwnerName)) then
+ has_possible_prop_pacs = true
end
- return
end
- elseif not data then
- ErrorNoHalt(("Decoding %s failed: %s\n"):format(name,err))
- return
end
- pace.LoadPartsFromTable(data, clear, override_part)
+ --queue up prop pacs for the next prop or npc you spawn when in singleplayer
+ if (auto_spawn_prop:GetInt() == 2 or (auto_spawn_prop:GetInt() == 1 and #data == 1)) and game.SinglePlayer() and has_possible_prop_pacs then
+ if clear then pace.ClearParts() end
+ LocalPlayer().pac_propload_queuedparts = LocalPlayer().pac_propload_queuedparts or {}
+
+ --check all root parts from data. format: each data member is a {self, children} table of the part and the list of children
+ for i, part in pairs(data) do
+ local possible_prop_pac = isnumber(tonumber(part.self.OwnerName))
+ if part.self.ClassName == "group" and possible_prop_pac then
+
+ part.self.ModelTracker = part.self.ModelTracker or ""
+ part.self.ClassTracker = part.self.ClassTracker or ""
+ local str = ""
+ if part.self.ClassTracker == "" or part.self.ClassTracker == "" then
+ str = "But the class or model is unknown"
+ else
+ str = part.self.ClassTracker .. " : " .. part.self.ModelTracker
+ end
+ --notify which model / entity should be spawned with the class tracker
+ notification.AddLegacy( "You have queued a pac part (" .. i .. ":" .. part.self.Name .. ") for a prop or NPC! " .. str, NOTIFY_HINT, 10 )
+ LocalPlayer().pac_propload_queuedparts[i] = part
+
+ else
+ pace.LoadPartsFromTable(part, false, false)
+ end
+ end
+
+ else
+ if name == "autoload" and (not data or not next(data)) then
+ data, err = pace.luadata.ReadFile("pac3/sessions/" .. name .. ".txt", nil, true)
+ if not data then
+ if err then
+ pace.MessagePrompt(err, "Autoload failed", "OK")
+ end
+ return
+ end
+ elseif not data then
+ pace.MessagePrompt(err, ("Decoding %s failed"):format(name), "OK")
+ return
+ end
+
+ pace.LoadPartsFromTable(data, clear, override_part)
+ end
end
end
end
-concommand.Add('pac_load_url', function(ply, cmd, args)
- if not args[1] then return print('[PAC3] No URL specified') end
+concommand.Add("pac_load_url", function(ply, cmd, args)
+ if not args[1] then return print("[PAC3] No URL specified") end
local url = args[1]:Trim()
- if not url:find("https?://") then return print('[PAC3] Invalid URL specified') end
- pac.Message('Loading specified URL')
+ if not url:find("https?://") then return print("[PAC3] Invalid URL specified") end
+ pac.Message("Loading specified URL")
if args[2] == nil then args[2] = '1' end
pace.LoadParts(url, tobool(args[2]))
end)
@@ -274,8 +342,8 @@ function pace.LoadPartsFromTable(data, clear, override_part)
pace.RefreshTree(true)
for i, part in ipairs(partsLoaded) do
- part:CallRecursive('OnOutfitLoaded')
- part:CallRecursive('PostApplyFixes')
+ part:CallRecursive("OnOutfitLoaded")
+ part:CallRecursive("PostApplyFixes")
end
pac.LocalPlayer.pac_fix_show_from_render = SysTime() + 1
@@ -288,10 +356,9 @@ local function add_files(tbl, dir)
if folders then
for key, folder in pairs(folders) do
- if folder == "__backup" or folder == "objcache" or folder == "__animations" or folder == "__backup_save" then goto CONTINUE end
+ if folder == "__backup" or folder == "objcache" or folder == "__animations" or folder == "__backup_save" then continue end
tbl[folder] = {}
add_files(tbl[folder], dir .. "/" .. folder)
- ::CONTINUE::
end
end
@@ -311,13 +378,13 @@ local function add_files(tbl, dir)
data.Path = path
data.RelativePath = (dir .. "/" .. data.Name):sub(2)
- local dat,err=pace.luadata.ReadFile(path)
+ local dat, err = pace.luadata.ReadFile(path)
data.Content = dat
if dat then
table.insert(tbl, data)
else
- pac.dprint(("Decoding %s failed: %s\n"):format(path,err))
+ pac.dprint(("Decoding %s failed: %s\n"):format(path, err))
chat.AddText(("Could not load: %s\n"):format(path))
end
@@ -326,7 +393,7 @@ local function add_files(tbl, dir)
end
end
- table.sort(tbl, function(a,b)
+ table.sort(tbl, function(a, b)
if a.Time and b.Time then
return a.Name < b.Name
end
@@ -383,7 +450,7 @@ end
local function populate_parts(menu, tbl, override_part, clear)
for key, data in pairs(tbl) do
if not data.Path then
- local menu, pnl = menu:AddSubMenu(key, function()end, data)
+ local menu, pnl = menu:AddSubMenu(key, function() end, data)
pnl:SetImage(pace.MiscIcons.load)
menu.GetDeleteSelf = function() return false end
local old = menu.Open
@@ -425,6 +492,39 @@ local function populate_parts(menu, tbl, override_part, clear)
end
end
+function pace.AddOneDirectorySavedPartsToMenu(menu, subdir, nicename)
+ if not subdir then return end
+ local subdir_head = subdir .. "/"
+
+ local exp_submenu, pnl = menu:AddSubMenu(L"" .. subdir)
+ pnl:SetImage(pace.MiscIcons.load)
+ exp_submenu.GetDeleteSelf = function() return false end
+ subdir = "pac3/" .. subdir
+ if nicename then exp_submenu:SetText(nicename) end
+
+ add_expensive_submenu_load(pnl, function(subdir)
+ local files = file.Find(subdir .. "/*", "DATA")
+ local files2 = {}
+ --PrintTable(files)
+ for i, filename in ipairs(files) do
+ table.insert(files2, {filename, file.Time(subdir .. filename, "DATA")})
+ end
+
+ table.sort(files2, function(a, b)
+ return a[2] > b[2]
+ end)
+
+ for _, data in pairs(files2) do
+ local name = data[1]
+ local full_path = subdir .. "/" .. name
+ --print(full_path)
+ local friendly_name = name .. " " .. string.NiceSize(file.Size(full_path, "DATA"))
+ exp_submenu:AddOption(friendly_name, function() pace.LoadParts(subdir_head .. name, true) end)
+ :SetImage(pace.MiscIcons.outfit)
+ end
+ end, subdir)
+end
+
function pace.AddSavedPartsToMenu(menu, clear, override_part)
menu.GetDeleteSelf = function() return false end
@@ -447,7 +547,7 @@ function pace.AddSavedPartsToMenu(menu, clear, override_part)
"",
function(name)
- local data,err = pace.luadata.Decode(name)
+ local data, _ = pace.luadata.Decode(name)
if data then
pace.LoadPartsFromTable(data, clear, override_part)
end
@@ -461,7 +561,7 @@ function pace.AddSavedPartsToMenu(menu, clear, override_part)
examples.GetDeleteSelf = function() return false end
local sorted = {}
- for k,v in pairs(pace.example_outfits) do sorted[#sorted + 1] = {k = k, v = v} end
+ for k, v in pairs(pace.example_outfits) do sorted[#sorted + 1] = {k = k, v = v} end
table.sort(sorted, function(a, b) return a.k < b.k end)
for _, data in pairs(sorted) do
@@ -481,7 +581,10 @@ function pace.AddSavedPartsToMenu(menu, clear, override_part)
pnl:SetImage(pace.MiscIcons.clone)
backups.GetDeleteSelf = function() return false end
- add_expensive_submenu_load(pnl, function()
+ local subdir = "pac3/__backup/*"
+
+ add_expensive_submenu_load(pnl, function(subdir)
+
local files = file.Find("pac3/__backup/*", "DATA")
local files2 = {}
@@ -500,14 +603,15 @@ function pace.AddSavedPartsToMenu(menu, clear, override_part)
backups:AddOption(friendly_name, function() pace.LoadParts("__backup/" .. name, true) end)
:SetImage(pace.MiscIcons.outfit)
end
- end)
+ end, subdir)
local backups, pnl = menu:AddSubMenu(L"outfit backups")
pnl:SetImage(pace.MiscIcons.clone)
backups.GetDeleteSelf = function() return false end
+ subdir = "pac3/__backup_save/*"
add_expensive_submenu_load(pnl, function()
- local files = file.Find("pac3/__backup_save/*", "DATA")
+ local files = file.Find(subdir, "DATA")
local files2 = {}
for i, filename in ipairs(files) do
@@ -534,7 +638,7 @@ function pace.AddSavedPartsToMenu(menu, clear, override_part)
end)
:SetImage(pace.MiscIcons.outfit)
end
- end)
+ end, subdir)
end
local function populate_parts(menu, tbl, dir, override_part)
@@ -570,7 +674,7 @@ local function populate_parts(menu, tbl, dir, override_part)
menu:AddSpacer()
for key, data in pairs(tbl) do
if not data.Path then
- local menu, pnl = menu:AddSubMenu(key, function()end, data)
+ local menu, pnl = menu:AddSubMenu(key, function() end, data)
pnl:SetImage(pace.MiscIcons.load)
menu.GetDeleteSelf = function() return false end
populate_parts(menu, data, dir .. "/" .. key, override_part)
@@ -607,11 +711,11 @@ local function populate_parts(menu, tbl, dir, override_part)
local function delete_directory(dir)
local files, folders = file.Find(dir .. "*", "DATA")
- for k,v in ipairs(files) do
+ for k, v in ipairs(files) do
file.Delete(dir .. v)
end
- for k,v in ipairs(folders) do
+ for k, v in ipairs(folders) do
delete_directory(dir .. v .. "/")
end
@@ -707,7 +811,7 @@ function pace.FixBadGrouping(data)
},
}
- for k,v in pairs(other) do
+ for k, v in pairs(other) do
table.insert(out, v)
end
diff --git a/lua/pac3/editor/client/select.lua b/lua/pac3/editor/client/select.lua
index 87de84cef..76c64384d 100644
--- a/lua/pac3/editor/client/select.lua
+++ b/lua/pac3/editor/client/select.lua
@@ -334,7 +334,23 @@ function pace.SelectBone(ent, callback, only_movable)
)
end
-function pace.SelectPart(parts, callback)
+function pace.SelectPart(parts, callback, property)
+ --mark some editor-related info for selecting the part via the editor labels
+ last_current_part = pace.current_part
+ pace.bypass_tree = true
+ pace.selecting_property_key = property.CurrentKey
+ pace.selecting_property = property
+ pac.AddHook("Tick", "selecting_part", function()
+ if not pace.selecting_property_key then pac.RemoveHook("Tick", "selecting_part") pace.bypass_tree = false end
+ if last_current_part ~= pace.current_part then --we've selected another part so
+ local new_select = pace.current_part
+ last_current_part["Set" .. pace.selecting_property_key](last_current_part, new_select)
+ pace.bypass_tree = false
+ timer.Simple(0.4, function() pace.OnPartSelected(last_current_part, false) end)
+ pace.StopSelect()
+ end
+ if not pace.IsSelecting then pac.RemoveHook("Tick", "selecting_part") pace.bypass_tree = false end
+ end)
select_something(
parts,
diff --git a/lua/pac3/editor/client/settings.lua b/lua/pac3/editor/client/settings.lua
index 574c99876..e2629afe2 100644
--- a/lua/pac3/editor/client/settings.lua
+++ b/lua/pac3/editor/client/settings.lua
@@ -1,34 +1,2560 @@
+include("parts.lua")
+include("shortcuts.lua")
+
+--{"cvar", "description", "tooltip", decimals, min, max} if decimals is -1 it's a bool
+local convar_params_combat_generic = {
+
+ --general sv protection
+ {"pac_sv_prop_protection", "Enforce generic prop protection for player-owned props and physics entities based on client consents.", "", -1, 0, 200},
+ {"pac_sv_combat_whitelisting", "Restrict new pac3 combat (damage zone, lock, force, hitscan, health modifier) to only whitelisted users.", "off = Blacklist mode: Default players are allowed to use the combat features\non = Whitelist mode: Default players aren't allowed to use the combat features until set to Allowed", -1, 0, 200},
+ {"pac_sv_block_combat_features_on_next_restart", "Block the combat features that aren't enabled. WARNING! Requires a restart!\nThis applies to damage zone, lock, force, hitscan and health modifier parts", "You can go to the console and set pac_sv_block_combat_features_on_next_restart to 2 to block everything.\nif you re-enable a blocked part, update with pac_sv_combat_reinitialize_missing_receivers", -1, 0, 200},
+ {"pac_sv_combat_enforce_netrate_monitor_serverside", "Enable serverside monitoring prints for allowance and rate limiters", "Enable serverside monitoring prints.\n0=let clients enforce their netrate allowance before sending messages\n1=the server will receive net messages and print the outcome.", -1, 0, 200},
+ {"pac_sv_combat_enforce_netrate", "Rate limiter (milliseconds)", "The milliseconds delay between net messages.\nIf this is 0, the allowance won't matter, otherwise early net messages use up the player's allowance.\nThe allowance regenerates gradually when unused, and one unit gets spent if the message is earlier than the rate limiter's delay.", 0, 0, 1000},
+ {"pac_sv_combat_enforce_netrate_buffersize", "Allowance, in number of messages", "Allowance:\nIf this is 0, only the time limiter will stop pac combat messages if they're too fast.\nOtherwise, players trying to use a pac combat message earlier will deduct 1 from the player's allowance, and only stop the messages if the allowance reaches 0.", 0, 0, 400},
+ {"pac_sv_entity_limit_per_combat_operation", "Hard entity limit to cutoff damage zones and force parts", "If the number of entities selected is more than this value, the whole operation gets dropped.\nThis is so that the server doesn't have to send huge amounts of entity updates to everyone.", 0, 0, 1000},
+ {"pac_sv_entity_limit_per_player_per_combat_operation", "Entity limit per player to cutoff damage zones and force parts", "When in multiplayer, with the server's player count, if the number of entities selected is more than this value, the whole operation gets dropped.\nThis is so that the server doesn't have to send huge amounts of entity updates to everyone.", 0, 0, 500},
+ {"pac_sv_player_limit_as_fraction_to_drop_damage_zone", "block damage zones targeting this fraction of players", "This applies when the zone covers more than 12 players. 0 is 0% of the server, 1 is 100%\nFor example, if this is at 0.5, there are 24 players and a damage zone covers 13 players, it will be blocked.", 2, 0, 1},
+ {"pac_sv_combat_distance_enforced", "distance to block combat actions that are too far", "The distance is compared between the action's origin and the player's position.\n0 to ignore.", 0, 0, 64000},
+
+}
+local convar_params_lock = {
+ {"pac_sv_lock", "Allow lock part", "", -1, 0, 200},
+ {"pac_sv_lock_teleport", "Allow lock part teleportation", "", -1, 0, 200},
+ {"pac_sv_lock_grab", "Allow lock part grabbing", "", -1, 0, 200},
+ {"pac_sv_lock_aim", "Allow lock part aiming", "", -1, 0, 200},
+ {"pac_sv_lock_allow_grab_ply", "Allow grabbing players", "", -1, 0, 200},
+ {"pac_sv_lock_allow_grab_npc", "Allow grabbing NPCs", "", -1, 0, 200},
+ {"pac_sv_lock_allow_grab_ent", "Allow grabbing other entities", "", -1, 0, 200},
+ {"pac_sv_lock_max_grab_radius", "Max lock part grab range", "", 0, 0, 5000},
+}
+local convar_params_damage_zone = {
+ {"pac_sv_damage_zone", "Allow damage zone", "", -1, 0, 200},
+ {"pac_sv_damage_zone_max_radius", "Max damage zone radius", "", 0, 0, 32767},
+ {"pac_sv_damage_zone_max_length", "Max damage zone length", "", 0, 0, 32767},
+ {"pac_sv_damage_zone_max_damage", "Max damage zone damage", "", 0, 0, 268435455},
+ {"pac_sv_damage_zone_allow_dissolve", "Allow damage entity dissolvers", "", -1, 0, 200},
+ {"pac_sv_damage_zone_allow_ragdoll_hitparts", "Allow ragdoll hitparts", "", -1, 0, 200},
+}
+local convar_params_force = {
+ {"pac_sv_force", "Allow force part", "", -1, 0, 200},
+ {"pac_sv_force_max_radius", "Max force radius", "", 0, 0, 32767},
+ {"pac_sv_force_max_length", "Max force length", "", 0, 0, 32767},
+ {"pac_sv_force_max_length", "Max force amount", "", 0, 0, 10000000},
+}
+local convar_params_hitscan = {
+ {"pac_sv_hitscan", "allow serverside bullets", "", -1, 0, 200},
+ {"pac_sv_hitscan_max_damage", "Max hitscan damage (per bullet, per multishot,\ndepending on the next setting)", "", 0, 0, 268435455},
+ {"pac_sv_hitscan_divide_max_damage_by_max_bullets", "force hitscans to distribute their total damage accross bullets. if off, every bullet does full damage; if on, adding more bullets doesn't do more damage", "", -1, 0, 200},
+ {"pac_sv_hitscan_max_bullets", "Maximum number of bullets for hitscan multishots", "", 0, 0, 500},
+}
+local convar_params_projectile = {
+ {"pac_sv_projectiles", "allow serverside physical projectiles", "", -1, 0, 200},
+ {"pac_sv_projectile_allow_custom_collision_mesh", "allow custom collision meshes for physical projectiles", "", -1, 0, 200},
+ {"pac_sv_projectile_max_phys_radius", "Max projectile physical radius", "", 0, 0, 4095},
+ {"pac_sv_projectile_max_damage_radius", "Max projectile damage radius", "", 0, 0, 4095},
+ {"pac_sv_projectile_max_attract_radius", "Max projectile attract radius", "", 0, 0, 100000000},
+ {"pac_sv_projectile_max_damage", "Max projectile damage", "", 0, 0, 100000000},
+ {"pac_sv_projectile_max_speed", "Max projectile speed", "", 0, 0, 50000},
+ {"pac_sv_projectile_max_mass", "Max projectile mass", "", 0, 0, 500000},
+}
+local convar_params_health_modifier = {
+ {"pac_sv_health_modifier", "Allow health modifier part", "", -1, 0, 200},
+ {"pac_sv_health_modifier_allow_maxhp", "Allow changing max health and max armor", "", -1, 0, 200},
+ {"pac_sv_health_modifier_max_hp_armor", "Maximum value for max health / armor modification", "", 0, 0, 100000000},
+ {"pac_sv_health_modifier_min_damagescaling", "Minimum combined damage multiplier allowed.\nNegative values lead to healing from damage.", "", 2, -10, 1},
+ {"pac_sv_health_modifier_extra_bars", "Allow extra healthbars", "What are those? It's like an armor layer that takes damage before it gets applied to the entity.", -1, 0, 200},
+ {"pac_sv_health_modifier_allow_counted_hits", "Allow extra healthbars counted hits mode", "1 EX HP absorbs 1 whole hit.", -1, 0, 200},
+ {"pac_sv_health_modifier_max_extra_bars_value", "Maximum combined value for extra healthbars", "", 0, 0, 100000000},
+}
+
+local convar_params_modifiers = {
+ {"pac_modifier_blood_color", "Blood", "", -1, 0, 200},
+ {"pac_allow_mdl", "MDL", "", -1, 0, 200},
+ {"pac_allow_mdl_entity", "Entity MDL", "", -1, 0, 200},
+ {"pac_modifier_model", "Entity model", "", -1, 0, 200},
+ {"pac_modifier_size", "Entity size", "", -1, 0, 200},
+}
+local convar_params_movement = {
+ --the playermovement enabler policy cvar is a form, not a slider nor a bool
+ {"pac_player_movement_allow_mass", "Allow Modify Mass", "", -1, 0, 200},
+ {"pac_player_movement_min_mass", "Mimnimum mass players can set for themselves", "", 0, 0, 1000000},
+ {"pac_player_movement_max_mass", "Maximum mass players can set for themselves", "", 0, 0, 1000000},
+ {"pac_player_movement_physics_damage_scaling", "Allow damage scaling of physics damage based on player's mass", "", -1, 0, 200},
+}
+local convar_params_wearing_drawing = {
+ {"pac_sv_draw_distance", "PAC server draw distance", "", 0, 0, 500000},
+ {"pac_submit_spam", "Limit pac_submit to prevent spam", "", -1, 0, 200},
+ {"pac_submit_limit", "limit of pac_submits", "", 0, 0, 100},
+ {"pac_onuse_only_force", "Players need to +USE on others to reveal outfits", "", -1, 0, 200},
+ {"pac_sv_prop_outfits", "allow prop / other player outfits", "0 = don't allow\n1 = allow applying outfits on props/npcs\n2 = allow applying outfits on other players", 0, 0, 2},
+}
+local convar_params_misc = {
+ {"sv_pac_webcontent_allow_no_content_length", "Players need to +USE on others to reveal outfits", "", -1, 0, 200},
+ {"pac_to_contraption_allow", "Allow PAC to contraption tool", "", -1, 0, 200},
+ {"pac_max_contraption_entities", "Entity limit for PAC to contraption", "", 0, 0, 200},
+ {"pac_restrictions", "restrict PAC editor camera movement", "", -1, 0, 200},
+}
+
+pace = pace
+
+pace.partmenu_categories_experimental = {
+ ["new!"] =
+ {
+ ["icon"] = "icon16/new.png",
+ ["interpolated_multibone"] = "interpolated_multibone",
+ ["damage_zone"] = "damage_zone",
+ ["hitscan"] = "hitscan",
+ ["lock"] = "lock",
+ ["force"] = "force",
+ ["health_modifier"] = "health_modifier",
+ },
+ ["logic"] =
+ {
+ ["icon"] = "icon16/server_chart.png",
+ ["proxy"] = "proxy",
+ ["command"] = "command",
+ ["event"] = "event",
+ ["text"] = "text",
+ ["link"] = "link",
+ },
+ ["scaffolds"] =
+ {
+ ["tooltip"] = "useful to build up structures with specific positioning rules",
+ ["icon"] = "map",
+ ["jiggle"] = "jiggle",
+ ["model2"] = "model2",
+ ["projectile"] = "projectile",
+ ["interpolated_multibone"] = "interpolated_multibone",
+ },
+ ["combat"] =
+ {
+ ["icon"] = "icon16/joystick.png",
+ ["damage_zone"] = "damage_zone",
+ ["hitscan"] = "hitscan",
+ ["projectile"] = "projectile",
+ ["lock"] = "lock",
+ ["force"] = "force",
+ ["health_modifier"] = "health_modifier",
+ ["player_movement"] = "player_movement",
+ },
+ ["animation"]=
+ {
+ ["icon"] = "icon16/world.png",
+ ["group"] = "group",
+ ["event"] = "event",
+ ["custom_animation"] = "custom_animation",
+ ["proxy"] = "proxy",
+ ["sprite"] = "sprite",
+ ["particle"] = "particle",
+ },
+ ["materials"] =
+ {
+ ["icon"] = "pace.MiscIcons.appearance",
+ ["material_3d"] = "material_3d",
+ ["material_2d"] = "material_2d",
+ ["material_refract"] = "material_refract",
+ ["material_eye refract"] = "material_eye refract",
+ ["submaterial"] = "submaterial",
+ },
+ ["entity"] =
+ {
+ ["icon"] = "icon16/cd_go.png",
+ ["bone3"] = "bone3",
+ ["custom_animation"] = "custom_animation",
+ ["gesture"] = "gesture",
+ ["entity2"] = "entity2",
+ ["poseparameter"] = "poseparameter",
+ ["camera"] = "camera",
+ ["holdtype"] = "holdtype",
+ ["effect"] = "effect",
+ ["player_config"] = "player_config",
+ ["player_movement"] = "player_movement",
+ ["animation"] = "animation",
+ ["submaterial"] = "submaterial",
+ ["faceposer"] = "faceposer",
+ ["flex"] = "flex",
+ ["material_3d"] = "material_3d",
+ ["weapon"] = "weapon",
+ },
+ ["model"] =
+ {
+ ["icon"] = "icon16/bricks.png",
+ ["jiggle"] = "jiggle",
+ ["physics"] = "physics",
+ ["animation"] = "animation",
+ ["bone3"] = "bone3",
+ ["effect"] = "effect",
+ ["submaterial"] = "submaterial",
+ ["clip2"] = "clip2",
+ ["halo"] = "halo",
+ ["material_3d"] = "material_3d",
+ ["model2"] = "model2",
+ },
+ ["modifiers"] =
+ {
+ ["icon"] = "icon16/connect.png",
+ ["fog"] = "fog",
+ ["motion_blur"] = "motion_blur",
+ ["halo"] = "halo",
+ ["clip2"] = "clip2",
+ ["bone3"] = "bone3",
+ ["poseparameter"] = "poseparameter",
+ ["material_3d"] = "material_3d",
+ ["proxy"]= "proxy",
+ },
+ ["effects"] =
+ {
+ ["icon"] = "icon16/wand.png",
+ ["sprite"] = "sprite",
+ ["sound2"] = "sound2",
+ ["effect"] = "effect",
+ ["halo"] = "halo",
+ ["particles"]= "particles",
+ ["sunbeams"]= "sunbeams",
+ ["beam"]= "beam",
+ ["projected_texture"]= "projected_texture",
+ ["decal"]= "decal",
+ ["text"]= "text",
+ ["trail2"]= "trail2",
+ ["sound"]= "sound",
+ ["woohoo"]= "woohoo",
+ ["light2"]= "light2",
+ ["shake"]= "shake",
+ }
+}
+
+pace.partmenu_categories_default =
+{
+ ["legacy"]=
+ {
+ ["icon"] = pace.GroupsIcons.legacy,
+ ["trail"]= "trail",
+ ["bone2"]= "bone2",
+ ["model"]= "model",
+ ["bodygroup"]= "bodygroup",
+ ["material"]= "material",
+ ["light"]= "light",
+ ["entity"]= "entity",
+ ["clip"]= "clip",
+ ["bone"]= "bone",
+ ["webaudio"]= "webaudio",
+ ["ogg"] = "ogg",
+ },
+ ["combat"]=
+ {
+ ["icon"] = pace.GroupsIcons.combat,
+ ["lock"]= "lock",
+ ["force"]= "force",
+ ["projectile"]= "projectile",
+ ["damage_zone"] = "damage_zone",
+ ["hitscan"] = "hitscan",
+ ["health_modifier"] = "health_modifier",
+ },
+ ["advanced"]=
+ {
+ ["icon"] = pace.GroupsIcons.advanced,
+ ["custom_animation"]= "custom_animation",
+ ["material_refract"]= "material_refract",
+ ["projectile"]= "projectile",
+ ["link"] = "link",
+ ["material_2d"] = "material_2d",
+ ["material_eye refract"] = "material_eye refract",
+ ["command"] ="command",
+ },
+ ["entity"]=
+ {
+ ["icon"] = pace.GroupsIcons.entity,
+ ["bone3"] = "bone3",
+ ["gesture"] = "gesture",
+ ["entity2"] ="entity2",
+ ["poseparameter"] = "poseparameter",
+ ["camera"] = "camera",
+ ["holdtype"]= "holdtype",
+ ["effect"] = "effect",
+ ["player_config"] = "player_config",
+ ["player_movement"] = "player_movement",
+ ["animation"] = "animation",
+ ["submaterial"] = "submaterial",
+ ["faceposer"] = "faceposer",
+ ["flex"] = "flex",
+ ["material_3d"] = "material_3d",
+ ["weapon"]= "weapon",
+ },
+ ["model"]=
+ {
+ ["icon"] = pace.GroupsIcons.model,
+ ["jiggle"] = "jiggle",
+ ["physics"] = "physics",
+ ["animation"]= "animation",
+ ["bone3"] = "bone3",
+ ["effect"] = "effect",
+ ["submaterial"] ="submaterial",
+ ["clip2"] = "clip2",
+ ["halo"] = "halo",
+ ["material_3d"] = "material_3d",
+ ["model2"]= "model2",
+ },
+ ["modifiers"]=
+ {
+ ["icon"] = pace.GroupsIcons.modifiers,
+ ["animation"] = "animation",
+ ["fog"] = "fog",
+ ["motion_blur"] = "motion_blur",
+ ["clip2"]= "clip2",
+ ["poseparameter"] = "poseparameter",
+ ["material_3d"] = "material_3d",
+ ["proxy"] = "proxy",
+ },
+ ["effects"]=
+ {
+ ["icon"] = pace.GroupsIcons.effects,
+ ["sprite"] = "sprite",
+ ["sound2"] = "sound2",
+ ["effect"] = "effect",
+ ["halo"] = "halo",
+ ["particles"]= "particles",
+ ["sunbeams"] = "sunbeams",
+ ["beam"] = "beam",
+ ["projected_texture"]= "projected_texture",
+ ["decal"] = "decal",
+ ["text"] = "text",
+ ["trail2"] = "trail2",
+ ["sound"] = "sound",
+ ["woohoo"] = "woohoo",
+ ["light2"] = "light2",
+ ["shake"] = "shake"
+ }
+}
+
+
+local function rebuild_bookmarks()
+ pace.bookmarked_ressources = pace.bookmarked_ressources or {}
+
+ --here's some default favorites
+ if not pace.bookmarked_ressources["models"] or table.IsEmpty(pace.bookmarked_ressources["models"]) then
+ pace.bookmarked_ressources["models"] = {
+ "models/pac/default.mdl",
+ "models/pac/plane.mdl",
+ "models/pac/circle.mdl",
+ "models/hunter/blocks/cube025x025x025.mdl",
+ "models/editor/axis_helper.mdl",
+ "models/editor/axis_helper_thick.mdl"
+ }
+ end
+
+ if not pace.bookmarked_ressources["sound"] or table.IsEmpty(pace.bookmarked_ressources["sound"]) then
+ pace.bookmarked_ressources["sound"] = {
+ "music/hl1_song11.mp3",
+ "npc/combine_gunship/dropship_engine_near_loop1.wav",
+ "ambient/alarms/warningbell1.wav",
+ "phx/epicmetal_hard7.wav",
+ "phx/explode02.wav"
+ }
+ end
+
+ if not pace.bookmarked_ressources["materials"] or table.IsEmpty(pace.bookmarked_ressources["materials"]) then
+ pace.bookmarked_ressources["materials"] = {
+ "models/debug/debugwhite",
+ "vgui/null",
+ "debug/env_cubemap_model",
+ "models/wireframe",
+ "cable/physbeam",
+ "cable/cable2",
+ "effects/tool_tracer",
+ "effects/flashlight/logo",
+ "particles/flamelet[1,5]",
+ "sprites/key_[0,9]",
+ "vgui/spawnmenu/generating",
+ "vgui/spawnmenu/hover"
+ }
+ end
+
+ if not pace.bookmarked_ressources["proxy"] or table.IsEmpty(pace.bookmarked_ressources["proxy"]) then
+ pace.bookmarked_ressources["proxy"] = {
+ --[[["user"] = {
+
+ },]]
+ ["fades and transitions"] ={
+ {
+ nicename = "standard clamp fade (in)",
+ expression = "clamp(timeex(),0,1)",
+ explanation = "the simplest fade.\nthis is normalized, which means you'll often multiply this whole unit by the amount you want, like a distance.\ntimeex() starts at 0, moves gradually to 1 and stops progressing at 1 due to the clamp"
+ },
+ {
+ nicename = "standard clamp fade (out)",
+ expression = "clamp(1 - timeex(),0,1)",
+ explanation = "the simplest fade's reverse.\nthis is normalized, which means you'll often multiply this whole unit by the amount you want, like a distance.\ntimeex() starts at 1, moves gradually to 0 and stops progressing at 0 due to the clamp"
+ },
+ {
+ nicename = "standard clamp fade (delayed in)",
+ expression = "clamp(-1 + timeex(),0,1)",
+ explanation = "the basic fade is delayed by the fact that the clamp makes sure the negative values are pulled back to 0 until the first argument crosses 0 into the clamp's range."
+ },
+ {
+ nicename = "standard clamp fade (delayed out)",
+ expression = "clamp(2 - timeex(),0,1)",
+ explanation = "the reverse fade is delayed by the fact that the clamp makes sure the values beyond 1 are pulled back to 1 until the first argument crosses 1 into the clamp's range."
+ },
+ {
+ nicename = "standard clamp fade (in and out)",
+ expression = "clamp(timeex(),0,1)*clamp(3 - timeex(),0,1)",
+ explanation = "this is just compounding both fades. the second clamp's 3 is comprised of 1 (the clamp max) + 1 (the delay BEFORE the fade) + 1 (the delay BETWEEN the fades)"
+ },
+ {
+ nicename = "quick ease setup",
+ expression = "easeInBack(clamp(timeex(),0,1))",
+ explanation = "get started quickly with the new easing functions.\nsearch \"ease\" in the proxy's input list to see how to write them in pac3, or look at the gmod wiki to see previews of each"
+ },
+ },
+ ["pulses"] = {
+ {
+ nicename = "bell pulse",
+ expression = "(0 + 1*sin(PI*timeex())^16)",
+ explanation = "a basic normalized pulse, using a sine power."
+ },
+ {
+ nicename = "square-like throb",
+ expression = "(0 + 1 * (cos(PI*timeex())^16) ^0.3)",
+ explanation = "a throbbing-like pulse, made by combining a sine power with a fractionnal power.\nthis is better explained visually, so either test it right here in game or go look at a graph to see how x, and cos or sin behave with powers.\ntry x^pow and sin(x)^pow, and try different pows"
+ },
+ {
+ nicename = "binary pulse",
+ expression = "floor(1 + sin(PI*timeex()))",
+ explanation = "an on-off pulse, in other words a square wave.\nthis one completes one cycle every 2 seconds.\nfloor rounds down between 1 and 0 with nothing in-between."
+ },
+ {
+ nicename = "saw wave (up)",
+ expression = "(timeex()%1)",
+ explanation = "a sawtooth wave. it can repeat a 0-1 transition."
+ },
+ {
+ nicename = "saw wave (down)",
+ expression = "(1 - timeex()%1)",
+ explanation = "a sawtooth wave. it can repeat a 1-0 transition."
+ },
+ {
+ nicename = "triangle wave",
+ expression = "(clamp(-1+timeex()%2,0,1) + clamp(1 - timeex()%2,0,1))",
+ explanation = "a triangle wave. it goes back and forth linearly like a saw up and down."
+ }
+ },
+ ["facial expressions"] = {
+ {
+ nicename = "normal slow blink",
+ expression = "3*clamp(sin(timeex())^100,0,1)",
+ explanation = "a normal slow blink.\nwhile flexes usually have a range of 0-1, the 3 outside of the clamp is there to trick the value into going faster in case they're too slow to reach their target"
+ },
+ {
+ nicename = "normal fast blink",
+ expression = "8*clamp(sin(timeex())^600,0,1)",
+ explanation = "a normal slow blink.\nwhile flexes usually have a range of 0-1, the 8 outside of the clamp is there to trick the value into going faster in case they're too slow to reach their target\nif it's still not enough, use another flex with less blinking amount to provide the additionnal distance for the blink"
+ },
+ {
+ nicename = "babble",
+ expression = "sin(12*timeex())^2",
+ explanation = "a basic piece to move the mouth semi-convincingly for voicelines.\nthere'll never be dynamic lipsync in pac3, but this is a start."
+ },
+ {
+ nicename = "voice smoothener",
+ expression = "clamp(feedback() + 70*voice_volume()*ftime() - 15*ftime(),0,2)",
+ explanation = "uses a feedback() setup to raise the mouth's value gradually against a constantly lowering value, which should be more smoothly than a direct input"
+ },
+ {
+ nicename = "look side (legacy symmetrical look)",
+ expression = "3*(-1 + 2*pose_parameter(\"head_yaw\"))",
+ explanation = "an expression to mimic the head's yaw"
+ },
+ {
+ nicename = "look side (new)",
+ expression = "pose_parameter_true(\"head_yaw\")",
+ explanation = "an expression to mimic the head's yaw, but it requires your model to have this standard pose parameter"
+ },
+ {
+ nicename = "look up",
+ expression = "(-1 + 2*owner_eye_angle_pitch())",
+ explanation = "an expression to mimic the head's pitch on a [-1,1] range"
+ },
+ {
+ nicename = "single eyeflex direction (up)",
+ expression = "-0.03*pose_parameter_true(\"head_pitch\")",
+ explanation = "plug into an eye_look_up flex or an eye bone with a higher multiplier"
+ },
+ {
+ nicename = "single eyeflex direction (down)",
+ expression = "0.03*pose_parameter_true(\"head_pitch\")",
+ explanation = "plug into an eye_look_down flex or an eye bone with a higher multiplier"
+ },
+ {
+ nicename = "single eyeflex direction (left)",
+ expression = "0.03*pose_parameter_true(\"head_yaw\")",
+ explanation = "plug into an eye_look_left flex or an eye bone with a higher multiplier"
+ },
+ {
+ nicename = "single eyeflex direction (right)",
+ expression = "-0.03*pose_parameter_true(\"head_yaw\")",
+ explanation = "plug into an eye_look_right flex or an eye bone with a higher multiplier"
+ },
+ },
+ ["spatial"] = {
+ {
+ nicename = "random position (cloud)",
+ expression = "150*random(-1,1),150*random(-1,1),150*random(-1,1)",
+ explanation = "position a part randomly across X,Y,Z\nbut constantly blinking everywhere, because random generates a new number every frame.\nyou should only use this for parts that emit things into the world"
+ },
+ {
+ nicename = "random position (once)",
+ expression = "150*random_once(0,-1,1),150*random_once(1,-1,1),150*random_once(2,-1,1)",
+ explanation = "position a part randomly across X,Y,Z\nbut once, because random_once only generates a number once.\nit, however, needs distinct numbers in the first arguments to distinguish them every time you write the function."
+ },
+ {
+ nicename = "distance-based fade",
+ expression = "clamp((250/500) + 1 - (eye_position_distance() / 500),0,1)",
+ explanation = "a fading based on the viewer's distance. 250 and 500 are the example distances, 250 is where the expression starts diminishing, and 750 is where we reach 0."
+ },
+ {
+ nicename = "distance between two points",
+ expression = "part_distance(uid1,uid2)",
+ explanation = "Trick question! You have some homework! You need to find out your parts' UIDs first.\ntry tools -> copy global id, then paste those in place of uid1 and uid2"
+ },
+ {
+ nicename = "revolution (orbit)",
+ expression = "150*sin(time()),150*cos(time()),0",
+ explanation = "Trick question! You might need to rearrange the expression depending on which coordinate system we're at. For a thing on a pos_noang bone, it works as is. But for something on your head, you would need to swap x and z\n0,150*cos(time()),150*sin(time())"
+ },
+ {
+ nicename = "spin",
+ expression = "0,360*time(),0",
+ explanation = "a simple spinner on Y"
+ }
+ },
+ ["experimental things"] = {
+ {
+ nicename = "control a boolean directly with an event",
+ expression = "event_alternative(uid1,0,1)",
+ explanation = "trick question! you need to find out your event's part UID first and substitute uid1\n"
+ },
+ {
+ nicename = "feedback system controlled with 2 events",
+ expression = "feedback() + ftime()*(event_alternative(uid1,0,1) + event_alternative(uid2,0,-1))",
+ explanation = "trick question! you need to find out your event parts' UIDs first and substitute uid1 and uid2.\nthe new event_alternative function gets an event's state\nwe can inject that into our feedback system to act as a positive or negative speed"
+ },
+ {
+ nicename = "basic if-else statement",
+ expression = "number_operator_alternative(1,\">\",0,100,50)",
+ explanation = "might be niche but here's a basic alternator thing, you can compare the 1st and 3rd args with numeric operators like \"above\", \"<\", \"=\", \"~=\" etc. to choose between the 4th and 5th args\nit goes like this\nnumber_operator_alternative(1,\">\",0,100,50)\nif 1>0, return 100, else return 50"
+ },
+ {
+ nicename = "pick from 3 random colors",
+ expression = "number_operator_alternative(random_once(1), \"<\", 0.333, 1, number_operator_alternative(random_once(1), \">\", 0.666, 1.0, 0.75)),number_operator_alternative(random_once(1), \"<\", 0.333, 1, number_operator_alternative(random_once(1), \">\", 0.666, 0.8, 0.65)),number_operator_alternative(random_once(1), \"<\", 0.333, 1, number_operator_alternative(random_once(1), \">\", 0.666, 1.0, 0.58))",
+ explanation =
+ "using a shared random source, you can nest number_operator_alternative functions to get a 3-way branching random choice\n0.333 and 0.666 correspond to the chance slices where each choice gets decided so you can change the probabilities by editing these numbers\nBecause of the fact we're going deep, it's not easily readable so I'll lay out each component.\n\n" ..
+ "R: number_operator_alternative(random_once(1), \"<\", 0.333, 1, number_operator_alternative(random_once(1), \">\", 0.666, 1.0, 0.75))\n"..
+ "G: number_operator_alternative(random_once(1), \"<\", 0.333, 1, number_operator_alternative(random_once(1), \">\", 0.666, 0.8, 0.65))\n"..
+ "B: number_operator_alternative(random_once(1), \"<\", 0.333, 1, number_operator_alternative(random_once(1), \">\", 0.666, 1.0, 0.58))\n\n"..
+ "The first choice is white (1,1,1), the second choice is light pink (1,0.8,1) like a strawberry milk, the third choice is light creamy brown (0.75,0.65,0.58) like chocolate milk"
+ },
+ {
+ nicename = "feedback command attractor",
+ expression = "feedback() + ftime()*(command(\"destination\") - feedback())",
+ explanation =
+ "This thing uses a principle of iteration similar to exponential functions to attract the feedback toward any target\n"..
+ "The delta bit will get smaller and smaller as the gap between destination and feedback closes, stabilizing at 0, thereby stopping.\n"..
+ "You will utilize pac_proxy commands to set the destination target: \"pac_proxy destination 2\" will make the expression tend toward 2."
+ }
+ }
+ }
+
+ end
+
+end
local PANEL = {}
+local player_ban_list = {}
+local player_combat_ban_list = {}
+
+local function encode_table_to_file(str)
+ local data = {}
+ if not file.Exists("pac3_config", "DATA") then
+ file.CreateDir("pac3_config")
+
+ end
+
+
+ if str == "pac_editor_shortcuts" then
+ data = pace.PACActionShortcut
+ file.Write("pac3_config/" .. str..".txt", util.TableToKeyValues(data))
+ elseif str == "pac_editor_partmenu_layouts" then
+ data = pace.operations_order
+ file.Write("pac3_config/" .. str..".txt", util.TableToJSON(data, true))
+ elseif str == "pac_part_categories" then
+ data = pace.partgroups
+ file.Write("pac3_config/" .. str..".txt", util.TableToKeyValues(data))
+ elseif str == "bookmarked_ressources" then
+ rebuild_bookmarks()
+ for category, tbl in pairs(pace.bookmarked_ressources) do
+ data = tbl
+ str = category
+ file.Write("pac3_config/bookmarked_" .. str..".txt", util.TableToKeyValues(data))
+ end
+ elseif str == "eventwheel_colors" then
+ data = pace.command_colors or {}
+ file.Write("pac3_config/" .. str..".txt", util.TableToJSON(data, true))
+ end
+
+end
+
+local function decode_table_from_file(str)
+ if str == "bookmarked_ressources" then
+ rebuild_bookmarks()
+ local ressource_types = {"models", "sound", "materials", "sprites"}
+ for _, category in pairs(ressource_types) do
+ data = file.Read("pac3_config/bookmarked_" .. category ..".txt", "DATA")
+ if data then pace.bookmarked_ressources[category] = util.KeyValuesToTable(data) end
+ end
+ return
+ end
+
+ local data = file.Read("pac3_config/" .. str..".txt", "DATA")
+ if not data then return end
+
+ if str == "pac_editor_shortcuts" then
+ pace.PACActionShortcut = util.KeyValuesToTable(data)
+
+ elseif str == "pac_editor_partmenu_layouts" then
+ pace.operations_order = util.JSONToTable(data)
+
+ elseif str == "pac_part_categories" then
+ pace.partgroups = util.KeyValuesToTable(data, false, true)
+
+ elseif str == "eventwheel_colors" then
+ if not util.JSONToTable(data) then
+ if not table.IsEmpty(util.KeyValuesToTable(data)) then
+ pace.command_colors = util.KeyValuesToTable(data)
+ end
+ else
+ pace.command_colors = util.JSONToTable(data)
+ end
+ end
+
+
+end
+
+decode_table_from_file("bookmarked_ressources")
+pace.bookmarked_ressources = pace.bookmarked_ressources or {}
+
+function pace.SaveRessourceBookmarks()
+ encode_table_to_file("bookmarked_ressources")
+end
+
function PANEL:Init()
- local pnl = vgui.Create("DPropertySheet", self)
- pnl:Dock(FILL)
+ local master_pnl = vgui.Create("DPropertySheet", self)
+ master_pnl:Dock(FILL)
- local props = pace.FillWearSettings(pnl)
+ local properties_filter = pace.FillWearSettings(master_pnl)
+ master_pnl:AddSheet("Wear / Ignore", properties_filter)
- pnl:AddSheet("Wear / Ignore", props)
- self.sheet = pnl
+ local editor_settings = pace.FillEditorSettings(master_pnl)
+ master_pnl:AddSheet("Editor menu Settings", editor_settings)
+
+ local editor_settings2 = pace.FillEditorSettings2(master_pnl)
+ master_pnl:AddSheet("Editor menu Settings 2", editor_settings2)
+
+
+ if game.SinglePlayer() or LocalPlayer():IsAdmin() then
+
+ local general_sv_settings = pace.FillServerSettings(master_pnl)
+ master_pnl:AddSheet("General Settings (SV)", general_sv_settings)
+
+ local combat_sv_settings = pace.FillCombatSettings(master_pnl)
+ master_pnl:AddSheet("Combat Settings (SV)", combat_sv_settings)
+
+ local ban_settings = pace.FillBanPanel(master_pnl)
+ master_pnl:AddSheet("Bans (SV)", ban_settings)
+
+ local combat_ban_settings = pace.FillCombatBanPanel(master_pnl)
+ master_pnl:AddSheet("Combat Bans (SV)", combat_ban_settings)
+ net.Start("pac_request_sv_cvars") net.SendToServer()
+ end
+
+
+ self.sheet = master_pnl
+
+ --local properties_shortcuts = pace.FillShortcutSettings(pnl)
+ --pnl:AddSheet("Editor Shortcuts", properties_shortcuts)
end
vgui.Register( "pace_settings", PANEL, "DPanel" )
-function pace.OpenSettings()
+function pace.OpenSettings(tab)
if IsValid(pace.settings_panel) then
pace.settings_panel:Remove()
end
local pnl = vgui.Create("DFrame")
pnl:SetTitle("pac settings")
pace.settings_panel = pnl
- pnl:SetSize(600,600)
+ pnl:SetSize(900,600)
pnl:MakePopup()
pnl:Center()
pnl:SetSizable(true)
-
+ pnl.OnClose = function()
+ if LocalPlayer():IsAdmin() and pace.cvar_changes then
+ local changes_str = ""
+ for cmd, val in pairs(pace.cvar_changes) do
+ if isbool(val) then
+ changes_str = changes_str .. cmd .. " set to " .. (val and "1" or "0") .. "\n"
+ else
+ changes_str = changes_str .. cmd .. " set to " .. val .. "\n"
+ end
+ end
+ Derma_Query("Send changes to the server?\n"..changes_str,table.Count(pace.cvar_changes) .. " server convars changed",
+ "Send changes to server", function()
+ for cmd, val in pairs(pace.cvar_changes) do
+ net.Start("pac_send_sv_cvar")
+ net.WriteString(cmd)
+ if isbool(val) then
+ net.WriteString(val and "1" or "0")
+ else
+ net.WriteString(val)
+ end
+ net.SendToServer()
+ end
+ pace.cvar_changes = nil
+ end,
+ "Cancel", function() pace.cvar_changes = nil end)
+ end
+ end
+ timer.Simple(0.5, function() pace.cvar_changes = nil end)
local pnl = vgui.Create("pace_settings", pnl)
pnl:Dock(FILL)
+ if tab then
+ pnl.sheet:SwitchToName(tab)
+ end
end
concommand.Add("pace_settings", function()
pace.OpenSettings()
-end)
\ No newline at end of file
+end)
+
+
+function pace.FillBanPanel(pnl)
+ local pnl = pnl
+ local BAN = vgui.Create("DPanel", pnl)
+ local ply_state_list = player_ban_list or {}
+
+ local ban_list = vgui.Create("DListView", BAN)
+ ban_list:SetText("ban list")
+ ban_list:SetSize(400,400)
+ ban_list:SetPos(10,10)
+
+ ban_list:AddColumn("Player name")
+ ban_list:AddColumn("SteamID")
+ ban_list:AddColumn("State")
+ ban_list:SetSortable(false)
+ for _,ply in pairs(player.GetAll()) do
+ --print(ply, pace.IsBanned(ply))
+ ban_list:AddLine(ply:Name(),ply:SteamID(),player_ban_list[ply] or "Allowed")
+ end
+
+ function ban_list:DoDoubleClick( lineID, line )
+ --MsgN( "Line " .. lineID .. " was double clicked!" )
+ local state = line:GetColumnText( 3 )
+
+ if state == "Banned" then state = "Allowed"
+ elseif state == "Allowed" then state = "Banned" end
+ line:SetColumnText(3,state)
+ ply_state_list[player.GetBySteamID(line:GetColumnText( 2 ))] = state
+ PrintTable(ply_state_list)
+ end
+
+ local ban_confirm_list_button = vgui.Create("DButton", BAN)
+ ban_confirm_list_button:SetText("Send ban list update to server")
+
+ ban_confirm_list_button:SetTooltip("WARNING! Unauthorized use will be notified to the server!")
+ ban_confirm_list_button:SetColor(Color(255,0,0))
+ ban_confirm_list_button:SetSize(200, 40)
+ ban_confirm_list_button:SetPos(450, 10)
+ function ban_confirm_list_button:DoClick()
+ net.Start("pac.BanUpdate")
+ net.WriteTable(ply_state_list)
+ net.SendToServer()
+ end
+ local ban_request_list_button = vgui.Create("DButton", BAN)
+ ban_request_list_button:SetText("Request ban list from server")
+ --ban_request_list_button:SetColor(Color(255,0,0))
+ ban_request_list_button:SetSize(200, 40)
+ ban_request_list_button:SetPos(450, 60)
+
+ function ban_request_list_button:DoClick()
+ net.Start("pac.RequestBanStates")
+ net.SendToServer()
+ end
+
+ net.Receive("pac.SendBanStates", function()
+ local players = net.ReadTable()
+ player_ban_list = players
+ PrintTable(players)
+ end)
+
+
+ return BAN
+end
+
+function pace.FillCombatBanPanel(pnl)
+ local pnl = pnl
+ local BAN = vgui.Create("DPanel", pnl)
+ pac.global_combat_whitelist = pac.global_combat_whitelist or {}
+
+
+ local ban_list = vgui.Create("DListView", BAN)
+ ban_list:SetText("Combat ban list")
+ ban_list:SetSize(400,400)
+ ban_list:SetPos(10,10)
+
+ ban_list:AddColumn("Player name")
+ ban_list:AddColumn("SteamID")
+ ban_list:AddColumn("State")
+ ban_list:SetSortable(false)
+ if GetConVar('pac_sv_combat_whitelisting'):GetBool() then
+ ban_list:SetTooltip( "Whitelist mode: Default players aren't allowed to use the combat features until set to Allowed" )
+ else
+ ban_list:SetTooltip( "Blacklist mode: Default players are allowed to use the combat features" )
+ end
+
+ local combat_bans_temp_merger = {}
+
+ for _,ply in pairs(player.GetAll()) do
+ combat_bans_temp_merger[ply:SteamID()] = pac.global_combat_whitelist[ply:SteamID()]-- or {nick = ply:Nick(), steamid = ply:SteamID(), permission = "Default"}
+ end
+
+ for id,data in pairs(pac.global_combat_whitelist) do
+ combat_bans_temp_merger[id] = data
+ end
+
+ for id,data in pairs(combat_bans_temp_merger) do
+ ban_list:AddLine(data.nick,data.steamid,data.permission)
+ end
+
+ function ban_list:DoDoubleClick( lineID, line )
+ --MsgN( "Line " .. lineID .. " was double clicked!" )
+ local state = line:GetColumnText( 3 )
+
+ if state == "Banned" then state = "Default"
+ elseif state == "Default" then state = "Allowed"
+ elseif state == "Allowed" then state = "Banned" end
+ line:SetColumnText(3,state)
+ pac.global_combat_whitelist[string.lower(line:GetColumnText( 2 ))].permission = state
+ PrintTable(pac.global_combat_whitelist)
+ end
+
+ local ban_confirm_list_button = vgui.Create("DButton", BAN)
+ ban_confirm_list_button:SetText("Send combat ban list update to server")
+
+ ban_confirm_list_button:SetTooltip("WARNING! Unauthorized use will be notified to the server!")
+ ban_confirm_list_button:SetColor(Color(255,0,0))
+ ban_confirm_list_button:SetSize(200, 40)
+ ban_confirm_list_button:SetPos(450, 10)
+ function ban_confirm_list_button:DoClick()
+ net.Start("pac.CombatBanUpdate")
+ net.WriteTable(pac.global_combat_whitelist)
+ net.WriteBool(true)
+ net.SendToServer()
+ end
+ local ban_request_list_button = vgui.Create("DButton", BAN)
+ ban_request_list_button:SetText("Request ban list from server")
+ --ban_request_list_button:SetColor(Color(255,0,0))
+ ban_request_list_button:SetSize(200, 40)
+ ban_request_list_button:SetPos(450, 60)
+
+ function ban_request_list_button:DoClick()
+ net.Start("pac.RequestCombatBanStates")
+ net.SendToServer()
+ end
+
+ net.Receive("pac.SendCombatBanStates", function()
+ pac.global_combat_whitelist = net.ReadTable()
+ ban_list:Clear()
+ local combat_bans_temp_merger = {}
+
+ for _,ply in pairs(player.GetAll()) do
+ combat_bans_temp_merger[ply:SteamID()] = pac.global_combat_whitelist[ply:SteamID()]-- or {nick = ply:Nick(), steamid = ply:SteamID(), permission = "Default"}
+ end
+
+ for id,data in pairs(pac.global_combat_whitelist) do
+ combat_bans_temp_merger[id] = data
+ end
+
+ for id,data in pairs(combat_bans_temp_merger) do
+ ban_list:AddLine(data.nick,data.steamid,data.permission)
+ end
+ end)
+
+
+ return BAN
+end
+local cvar_panels = {}
+
+
+local function PopulateCategory(str, pnl, cvars_tbl)
+ --create a collapsible header category
+ local list = pnl:Add(str)
+ list.Header:SetSize(40,40)
+ list.Header:SetFont("DermaLarge")
+ local list_list = vgui.Create("DListLayout")
+ list_list:DockPadding(20,0,20,20)
+ list:SetContents(list_list)
+
+ --insert the cvars for the category
+ for i, tbl in ipairs(cvars_tbl) do
+ local cvar_pnl
+ if tbl[4] == -1 then
+ cvar_pnl = vgui.Create("DCheckBoxLabel", list_list)
+ cvar_pnl.OnChange = function(self, val)
+ pace.cvar_changes = pace.cvar_changes or {}
+ pace.cvar_changes[tbl[1]] = val
+ end
+ else
+ cvar_pnl = vgui.Create("DNumSlider", list_list)
+ cvar_pnl:SetDecimals(tbl[4])
+ cvar_pnl:SetMin(tbl[5])
+ cvar_pnl:SetMax(tbl[6])
+ cvar_pnl.OnValueChanged = function(self, val)
+ pace.cvar_changes = pace.cvar_changes or {}
+ pace.cvar_changes[tbl[1]] = val
+ end
+ end
+ cvar_panels[tbl[1]] = cvar_pnl
+ cvar_pnl:SetText(tbl[2])
+ if tbl[3] ~= "" then cvar_pnl:SetTooltip(tbl[3]) end
+ cvar_pnl:SetSize(400,30)
+
+ end
+ return list_list
+end
+
+net.Receive("pac_send_cvars_to_client", function()
+ local cvars_tbl = net.ReadTable()
+ for cmd, val in pairs(cvars_tbl) do
+ if cvar_panels[cmd] then
+ --print("cvar exists " .. cmd .. " = " .. val)
+ cvar_panels[cmd]:SetValue(val)
+ else
+ --print("wrong cvar? " .. cmd)
+ end
+ end
+ pace.cvar_changes = nil
+end)
+
+function pace.FillCombatSettings(pnl)
+ local pnl = pnl
+
+ local master_list = vgui.Create("DCategoryList", pnl)
+ master_list:Dock(FILL)
+
+ --general
+ PopulateCategory("General (Global policy and Network protections)", master_list, convar_params_combat_generic)
+
+ --combat parts
+ PopulateCategory("Force part", master_list, convar_params_force)
+ PopulateCategory("Damage Zone", master_list, convar_params_damage_zone)
+ PopulateCategory("Lock part", master_list, convar_params_lock)
+ PopulateCategory("Hitscan part", master_list, convar_params_hitscan)
+ PopulateCategory("Projectiles", master_list, convar_params_projectile)
+ PopulateCategory("Health modifier part", master_list, convar_params_health_modifier)
+
+ return master_list
+end
+
+function pace.FillServerSettings(pnl)
+ local pnl = pnl
+
+ local master_list = vgui.Create("DCategoryList", pnl)
+ master_list:Dock(FILL)
+
+ --general server stuff
+ PopulateCategory("Allowed Playermodel Mutations", master_list, convar_params_modifiers)
+
+ --player movement stuff
+ local movement_category_list = PopulateCategory("Player Movement", master_list, convar_params_movement)
+ local pac_allow_movement_form = vgui.Create("DComboBox", movement_category_list)
+ pac_allow_movement_form:SetText("Allow PAC player movement")
+ pac_allow_movement_form:SetSize(400, 30)
+ pac_allow_movement_form:SetSortItems(false)
+
+ pac_allow_movement_form:AddChoice("disabled")
+ pac_allow_movement_form:AddChoice("disabled if noclip not allowed")
+ pac_allow_movement_form:AddChoice("enabled")
+
+ pac_allow_movement_form.OnSelect = function(_, _, value)
+ if value == "disabled" then
+ net.Start("pac_send_sv_cvar")
+ net.WriteString("pac_free_movement")
+ net.WriteString("0")
+ net.SendToServer()
+ --pac_allow_movement_form.form = generic_form("PAC player movement is disabled.")
+ elseif value == "disabled if noclip not allowed" then
+ net.Start("pac_send_sv_cvar")
+ net.WriteString("pac_free_movement")
+ net.WriteString("-1")
+ net.SendToServer()
+ --pac_allow_movement_form.form = generic_form("PAC player movement is disabled if noclip is not allowed.")
+ elseif value == "enabled" then
+ net.Start("pac_send_sv_cvar")
+ net.WriteString("pac_free_movement")
+ net.WriteString("1")
+ net.SendToServer()
+ --pac_allow_movement_form.form = generic_form("PAC player movement is enabled.")
+ end
+ end
+
+ PopulateCategory("Server wearing/drawing", master_list, convar_params_wearing_drawing)
+ PopulateCategory("Misc", master_list, convar_params_misc)
+
+ return master_list
+end
+
+
+--part order, shortcuts
+function pace.FillEditorSettings(pnl)
+
+ local buildlist_partmenu = {}
+ local f = vgui.Create( "DPanel", pnl )
+ f:SetSize(800)
+ f:Center()
+
+ local LeftPanel = vgui.Create( "DPanel", f ) -- Can be any panel, it will be stretched
+
+ local partmenu_order_presets = vgui.Create("DComboBox",LeftPanel)
+ partmenu_order_presets:SetText("Select a part menu preset")
+ partmenu_order_presets:AddChoice("factory preset")
+ partmenu_order_presets:AddChoice("legacy")
+ partmenu_order_presets:AddChoice("expanded PAC4.5 preset")
+ partmenu_order_presets:AddChoice("bulk select poweruser")
+ partmenu_order_presets:AddChoice("user preset")
+ partmenu_order_presets:SetX(10) partmenu_order_presets:SetY(10)
+ partmenu_order_presets:SetWidth(200)
+ partmenu_order_presets:SetHeight(20)
+
+ local partmenu_apply_button = vgui.Create("DButton", LeftPanel)
+ partmenu_apply_button:SetText("Apply")
+ partmenu_apply_button:SetX(220)
+ partmenu_apply_button:SetY(10)
+ partmenu_apply_button:SetWidth(65)
+ partmenu_apply_button:SetImage('icon16/accept.png')
+
+ local partmenu_clearlist_button = vgui.Create("DButton", LeftPanel)
+ partmenu_clearlist_button:SetText("Clear")
+ partmenu_clearlist_button:SetX(285)
+ partmenu_clearlist_button:SetY(10)
+ partmenu_clearlist_button:SetWidth(65)
+ partmenu_clearlist_button:SetImage('icon16/application_delete.png')
+
+ local partmenu_savelist_button = vgui.Create("DButton", LeftPanel)
+ partmenu_savelist_button:SetText("Save")
+ partmenu_savelist_button:SetX(350)
+ partmenu_savelist_button:SetY(10)
+ partmenu_savelist_button:SetWidth(70)
+ partmenu_savelist_button:SetImage('icon16/disk.png')
+
+
+
+ local partmenu_choices = vgui.Create("DScrollPanel", LeftPanel)
+ local partmenu_choices_textAdd = vgui.Create("DLabel", LeftPanel)
+ partmenu_choices_textAdd:SetText("ADD MENU COMPONENTS")
+ partmenu_choices_textAdd:SetFont("DermaDefaultBold")
+ partmenu_choices_textAdd:SetColor(Color(0,200,0))
+ partmenu_choices_textAdd:SetWidth(200)
+ partmenu_choices_textAdd:SetX(10)
+ partmenu_choices_textAdd:SetY(30)
+
+ local partmenu_choices_textRemove = vgui.Create("DLabel", LeftPanel)
+ partmenu_choices_textRemove:SetText("DOUBLE CLICK TO REMOVE")
+ partmenu_choices_textRemove:SetColor(Color(200,0,0))
+ partmenu_choices_textRemove:SetFont("DermaDefaultBold")
+ partmenu_choices_textRemove:SetWidth(200)
+ partmenu_choices_textRemove:SetX(220)
+ partmenu_choices_textRemove:SetY(30)
+
+ local partmenu_previews = vgui.Create("DListView", LeftPanel)
+ partmenu_previews:AddColumn("index")
+ partmenu_previews:AddColumn("control name")
+ partmenu_previews:SetSortable(false)
+ partmenu_previews:SetX(220)
+ partmenu_previews:SetY(50)
+ partmenu_previews:SetHeight(320)
+ partmenu_previews:SetWidth(200)
+
+
+
+ local shortcutaction_choices = vgui.Create("DComboBox", LeftPanel)
+ shortcutaction_choices:SetText("Select a PAC action")
+ shortcutaction_choices:SetSortItems(false)
+ local function rebuild_shortcut_box()
+ local display, active_action = shortcutaction_choices:GetSelected()
+ local active_action_count = 0
+ if pace.PACActionShortcut[active_action] and (table.Count(pace.PACActionShortcut[active_action]) > 0) then
+ active_action_count = table.Count(pace.PACActionShortcut[active_action])
+ end
+ for i=#pace.PACActionShortcut_Dictionary,1,-1 do
+ shortcutaction_choices:RemoveChoice(i)
+ end
+ for _,name in ipairs(pace.PACActionShortcut_Dictionary) do
+ local display_name = name
+ local binds_str = ""
+ if pace.PACActionShortcut[name] and (table.Count(pace.PACActionShortcut[name]) > 0) then
+ display_name = "[" .. table.Count(pace.PACActionShortcut[name]) .. "] " .. name
+ end
+ shortcutaction_choices:AddChoice(display_name, name)
+ end
+ if active_action then
+ if active_action_count > 0 then
+ timer.Simple(0, function() shortcutaction_choices:SetText("[" .. active_action_count .. "] " .. active_action) end)
+ end
+ end
+ end
+
+ local shortcut_dumps = {}
+ local shortcut_dumps_rawstring = ""
+ local function refresh_shortcut_dumps()
+ shortcut_dumps = {}
+ shortcut_dumps_rawstring = ""
+ for _,name in ipairs(pace.PACActionShortcut_Dictionary) do
+ local already_included_basename = false
+ if pace.PACActionShortcut[name] then
+ local binds_str = ""
+ for i=1,10,1 do
+ if pace.PACActionShortcut[name][i] then
+ if not already_included_basename then
+ shortcut_dumps_rawstring = shortcut_dumps_rawstring .. "\n" .. name .. " : "
+ already_included_basename = true
+ end
+ local raw_combo = {}
+ local combo_string = "["..i.."] = "
+ for j=1,10,1 do
+ if not pace.PACActionShortcut[name][i][j] then continue end
+ combo_string = combo_string .. pace.PACActionShortcut[name][i][j] .. " + "
+ table.insert(raw_combo, pace.PACActionShortcut[name][i][j])
+ end
+ if not table.IsEmpty(raw_combo) then
+ shortcut_dumps_rawstring = shortcut_dumps_rawstring ..
+ " {" .. table.concat(raw_combo, "+") .. "},"
+ end
+ if combo_string ~= "" then
+ combo_string = string.TrimRight(combo_string, " + ")
+ binds_str = binds_str .. "\n" .. combo_string
+ end
+ end
+ end
+ shortcut_dumps[name] = string.Trim(binds_str,"\n")
+ end
+ shortcut_dumps_rawstring = string.Trim(shortcut_dumps_rawstring,"\n")
+ end
+
+ local name, value = shortcutaction_choices:GetSelected()
+ shortcutaction_choices:SetTooltip(shortcut_dumps[value])
+ rebuild_shortcut_box()
+ end
+
+ local function get_common_keybind_groupings(filter)
+ if filter then
+ if table.IsEmpty(filter) then return get_common_keybind_groupings() end
+ end
+
+ local ctrl = {}
+ local shift = {}
+ local alt = {}
+ local ctrl_shift = {}
+ local ctrl_alt = {}
+ local shift_alt = {}
+ local singles = {}
+ local pass_filter = {}
+
+ for action,tbl in pairs(pace.PACActionShortcut) do
+ for i=1,10,1 do
+ if pace.PACActionShortcut[action][i] then
+
+ local raw_combo = {}
+ local contains_ctrl = false
+ local contains_shift = false
+ local contains_alt = false
+ local fail_filter = true
+ local filter_match_number = 0
+
+ for j=1,10,1 do
+ key = pace.PACActionShortcut[action][i][j]
+ if not key then continue end
+ table.insert(raw_combo, pace.PACActionShortcut[action][i][j])
+ if filter then
+ for _,k in ipairs(filter) do
+ if input.GetKeyCode(key) == k or key == k then
+ filter_match_number = filter_match_number + 1
+ end
+ end
+ else
+ if input.GetKeyCode(key) == KEY_LCONTROL or input.GetKeyCode(key) == KEY_RCONTROL then
+ contains_ctrl = true
+ elseif input.GetKeyCode(key) == KEY_LSHIFT or input.GetKeyCode(key) == KEY_RSHIFT then
+ contains_shift = true
+ elseif input.GetKeyCode(key) == KEY_LALT or input.GetKeyCode(key) == KEY_RALT then
+ contains_alt = true
+ end
+ end
+ end
+
+ if filter then
+ if filter_match_number == #filter then
+ table.insert(pass_filter, {raw_combo, action})
+ end
+ end
+
+ if not table.IsEmpty(raw_combo) then
+ if contains_ctrl then
+ table.insert(ctrl, {raw_combo, action})
+ if contains_shift then
+ table.insert(shift, {raw_combo, action})
+ table.insert(ctrl_shift, {raw_combo, action})
+ if contains_alt then
+ table.insert(shift_alt, {raw_combo, action})
+ end
+ end
+ if contains_alt then
+ table.insert(alt, {raw_combo, action})
+ table.insert(ctrl_alt, {raw_combo, action})
+ end
+ elseif contains_shift then
+ table.insert(shift, {raw_combo, action})
+ if contains_alt then
+ table.insert(alt, {raw_combo, action})
+ table.insert(shift_alt, {raw_combo, action})
+ end
+ elseif contains_alt then
+ table.insert(alt, {raw_combo, action})
+ else
+ table.insert(singles, {raw_combo, action})
+ end
+ end
+ end
+ end
+ end
+
+ return {
+ ctrl = ctrl,
+ shift = shift,
+ alt = alt,
+ ctrl_shift = ctrl_shift,
+ ctrl_alt = ctrl_alt,
+ shift_alt = shift_alt,
+ singles = singles,
+ pass_filter = pass_filter
+ }
+ end
+
+ local output_panel_scroll = vgui.Create("DScrollPanel", LeftPanel)
+ output_panel_scroll:SetPos(430, 10) output_panel_scroll:SetSize(500, 500)
+ local output_panel
+ local function create_richtext()
+ if IsValid(output_panel) then output_panel:Remove() end
+ output_panel = vgui.Create("RichText", output_panel_scroll)
+ output_panel_scroll:AddItem(output_panel)
+ output_panel_scroll:SetVerticalScrollbarEnabled(true)
+ output_panel:SetSize(900 - 430 - 200,500)
+ output_panel:InsertColorChange(0,0,0, 255)
+ end
+ create_richtext()
+ output_panel:AppendText("keybind viewer\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n<- Use this dropdown to filter")
+
+ local test_cbox = vgui.Create("DComboBox", LeftPanel)
+ test_cbox:SetPos(300, 440) test_cbox:SetSize(120, 20)
+ test_cbox:SetSortItems(false)
+ test_cbox:SetText("filter keybinds")
+ local binder1_value
+ local binder2_value
+ local binder3_value
+ function test_cbox:OnSelect(i,val,data)
+ create_richtext()
+ local keybind_groupings = get_common_keybind_groupings()
+
+ if keybind_groupings then
+ local str = ""
+ if val == "all actions" then
+ refresh_shortcut_dumps()
+ str = shortcut_dumps_rawstring
+ elseif val == "keybind categories" then
+ for groupname,group in pairs(keybind_groupings) do
+ str = str .. "Category: " .. groupname .. "\n"
+ for j,v in ipairs(group) do
+ str = str .. table.concat(v[1], "+") .. " : " .. v[2] .. "\n"
+ end
+ str = str .. "\n\n"
+ end
+ elseif val == "<<< filter" then
+ if not binder1_value then
+ output_panel:InsertColorChange(255,0,0, 255)
+ output_panel:AppendText("No binds to search! Use the three binder buttons\n")
+ output_panel:InsertColorChange(0,0,0, 255)
+ end
+ keybind_groupings = get_common_keybind_groupings({binder1_value, binder2_value, binder3_value})
+ if table.IsEmpty(keybind_groupings["pass_filter"]) then
+ output_panel:InsertColorChange(100,100,100, 255)
+ output_panel:AppendText("\n")
+ output_panel:InsertColorChange(0,0,0, 255)
+ end
+ for i,v in ipairs(keybind_groupings["pass_filter"]) do
+ str = str .. table.concat(v[1], "+") .. " : " .. v[2]
+ str = str .. "\n"
+ end
+ else
+ for i,v in ipairs(keybind_groupings[val]) do
+ str = str .. table.concat(v[1], "+") .. " : " .. v[2]
+ str = str .. "\n"
+ end
+ end
+ output_panel:AppendText(str)
+ end
+
+ end
+ test_cbox:AddChoice("<<< filter") test_cbox:AddChoice("all actions") test_cbox:AddChoice("keybind categories") test_cbox:AddChoice("singles")
+ test_cbox:AddChoice("ctrl") test_cbox:AddChoice("shift") test_cbox:AddChoice("alt")
+ test_cbox:AddChoice("ctrl_alt") test_cbox:AddChoice("ctrl_shift") test_cbox:AddChoice("shift_alt")
+
+ rebuild_shortcut_box()
+ shortcutaction_choices:SetX(10) shortcutaction_choices:SetY(400)
+ shortcutaction_choices:SetWidth(170)
+ shortcutaction_choices:SetHeight(20)
+ shortcutaction_choices:ChooseOptionID(1)
+ refresh_shortcut_dumps()
+ shortcutaction_choices:SetTooltip(shortcut_dumps_rawstring)
+ function shortcutaction_choices:OnMenuOpened(menu)
+ refresh_shortcut_dumps()
+ self:SetTooltip()
+ end
+ function shortcutaction_choices:Think()
+ self.next = self.next or 0
+ self.found = self.found or false
+ if self.next < RealTime() then self.found = false end
+ if self:IsHovered() then
+ if self:IsMenuOpen() then
+ self:SetTooltip()
+ end
+
+ if input.IsKeyDown(KEY_UP) then
+ refresh_shortcut_dumps()
+ if not self.found then self:ChooseOptionID(math.Clamp(self:GetSelectedID() + 1,1,table.Count(pace.PACActionShortcut_Dictionary))) self.found = true self.next = RealTime() + 0.3 end
+ elseif input.IsKeyDown(KEY_DOWN) then
+ refresh_shortcut_dumps()
+ if not self.found then self:ChooseOptionID(math.Clamp(self:GetSelectedID() - 1,1,table.Count(pace.PACActionShortcut_Dictionary))) self.found = true self.next = RealTime() + 0.3 end
+ else self.found = false end
+ else
+ self.found = false
+ end
+ end
+
+ local shortcuts_description_text = vgui.Create("DLabel", LeftPanel)
+ shortcuts_description_text:SetFont("DermaDefaultBold")
+ shortcuts_description_text:SetText("Edit keyboard shortcuts")
+ shortcuts_description_text:SetColor(Color(0,0,0))
+ shortcuts_description_text:SetWidth(200)
+ shortcuts_description_text:SetX(10)
+ shortcuts_description_text:SetY(380)
+
+ local shortcutaction_presets = vgui.Create("DComboBox", LeftPanel)
+ shortcutaction_presets:SetText("Select a shortcut preset")
+ shortcutaction_presets:AddChoice("factory preset", pace.PACActionShortcut_Default)
+ shortcutaction_presets:AddChoice("no CTRL preset", pace.PACActionShortcut_NoCTRL)
+ shortcutaction_presets:AddChoice("experimental preset", pace.PACActionShortcut_Experimental)
+
+ for i,filename in ipairs(file.Find("pac3_config/pac_editor_shortcuts*.txt","DATA")) do
+ local data = file.Read("pac3_config/" .. filename, "DATA")
+ shortcutaction_presets:AddChoice(string.GetFileFromFilename(filename), util.KeyValuesToTable(data))
+ end
+
+ shortcutaction_presets:SetX(10) shortcutaction_presets:SetY(420)
+ shortcutaction_presets:SetWidth(170)
+ shortcutaction_presets:SetHeight(20)
+ function shortcutaction_presets:OnSelect(num, name, data)
+ pace.PACActionShortcut = data
+ pace.FlashNotification("Selected shortcut preset: " .. name .. ". View console for more info")
+ pac.Message("Selected shortcut preset: " .. name)
+ for i,v in pairs(data) do
+ if #v > 0 then MsgC(Color(50,250,50), i .. "\n") end
+ for i2,v2 in pairs(v) do
+ MsgC(Color(0,250,250), "\t" .. table.concat(v2, "+") .. "\n")
+ end
+ end
+ end
+
+
+ local shortcutaction_choices_textCurrentShortcut = vgui.Create("DLabel", LeftPanel)
+ shortcutaction_choices_textCurrentShortcut:SetText("Shortcut to edit:")
+ shortcutaction_choices_textCurrentShortcut:SetColor(Color(0,60,160))
+ shortcutaction_choices_textCurrentShortcut:SetWidth(200)
+ shortcutaction_choices_textCurrentShortcut:SetX(200)
+ shortcutaction_choices_textCurrentShortcut:SetY(420)
+
+
+ local shortcutaction_index = vgui.Create("DNumberWang", LeftPanel)
+ shortcutaction_index:SetToolTip("index")
+ shortcutaction_index:SetValue(1)
+ shortcutaction_index:SetMin(1)
+ shortcutaction_index:SetMax(10)
+ shortcutaction_index:SetWidth(30)
+ shortcutaction_index:SetHeight(20)
+ shortcutaction_index:SetX(180)
+ shortcutaction_index:SetY(400)
+
+ local function update_shortcutaction_choices_textCurrentShortcut(num)
+ shortcutaction_choices_textCurrentShortcut:SetText("")
+ num = tonumber(num)
+ local diplayname, action = shortcutaction_choices:GetSelected()
+ local strs = {}
+
+ if action and action ~= "" then
+ if pace.PACActionShortcut[action] and pace.PACActionShortcut[action][num] then
+ for i,v in ipairs(pace.PACActionShortcut[action][num]) do
+ strs[i] = v
+ end
+ shortcutaction_choices_textCurrentShortcut:SetText("Shortcut to edit: " .. table.concat(strs, " + "))
+ else
+ shortcutaction_choices_textCurrentShortcut:SetText("")
+ end
+ end
+ end
+ update_shortcutaction_choices_textCurrentShortcut(1)
+
+ function shortcutaction_index:OnValueChanged(num)
+ update_shortcutaction_choices_textCurrentShortcut(num)
+ end
+
+ function shortcutaction_choices:OnSelect(i, displayname, action)
+ shortcutaction_index:OnValueChanged(shortcutaction_index:GetValue())
+ refresh_shortcut_dumps()
+ create_richtext()
+ output_panel:AppendText(action .. "\n")
+ output_panel:AppendText(shortcut_dumps[action] or "")
+ end
+
+ local binder1 = vgui.Create("DBinder", LeftPanel)
+ binder1:SetX(10)
+ binder1:SetY(440)
+ binder1:SetHeight(30)
+ binder1:SetWidth(90)
+ function binder1:OnChange( num )
+ if not num or num == 0 then binder1_value = nil return end
+ if not input.GetKeyName( num ) then return end
+ binder1_value = num
+ LocalPlayer():ChatPrint("New bound key 1: "..input.GetKeyName( num ))
+ pace.FlashNotification("New bound key 1: "..input.GetKeyName( num ))
+ end
+
+ local binder2 = vgui.Create("DBinder", LeftPanel)
+ binder2:SetX(105)
+ binder2:SetY(440)
+ binder2:SetHeight(30)
+ binder2:SetWidth(90)
+ function binder2:OnChange( num )
+ if not num or num == 0 then binder2_value = nil return end
+ if not input.GetKeyName( num ) then return end
+ binder2_value = num
+ LocalPlayer():ChatPrint("New bound key 2: "..input.GetKeyName( num ))
+ pace.FlashNotification("New bound key 2: "..input.GetKeyName( num ))
+ end
+
+ local binder3 = vgui.Create("DBinder", LeftPanel)
+ binder3:SetX(200)
+ binder3:SetY(440)
+ binder3:SetHeight(30)
+ binder3:SetWidth(90)
+ function binder3:OnChange( num )
+ if not num or num == 0 then binder3_value = nil return end
+ if not input.GetKeyName( num ) then return end
+ binder2_value = num
+ LocalPlayer():ChatPrint("New bound key 3: "..input.GetKeyName( num ))
+ pace.FlashNotification("New bound key 3: "..input.GetKeyName( num ))
+ end
+
+ local function send_active_shortcut_to_assign(tbl)
+ local display, action = shortcutaction_choices:GetSelected()
+ local index = shortcutaction_index:GetValue()
+ if not tbl then
+ pace.PACActionShortcut[action] = pace.PACActionShortcut[action] or {}
+ pace.PACActionShortcut[action][index] = pace.PACActionShortcut[action][index] or {}
+
+ if table.IsEmpty(pace.PACActionShortcut[action][index]) then
+ pace.PACActionShortcut[action][index] = nil
+ if table.IsEmpty(pace.PACActionShortcut[action]) then
+ pace.PACActionShortcut[action] = nil
+ end
+ else
+ pace.PACActionShortcut[action][index] = nil
+ end
+ elseif not table.IsEmpty(tbl) then
+ pace.AssignEditorShortcut(action, tbl, shortcutaction_index:GetValue())
+ end
+ encode_table_to_file("pac_editor_shortcuts")
+ end
+
+ local bindclear = vgui.Create("DButton", LeftPanel)
+ bindclear:SetText("clear")
+ bindclear:SetTooltip("deletes the current shortcut at the current index")
+ bindclear:SetX(10)
+ bindclear:SetY(480)
+ bindclear:SetHeight(30)
+ bindclear:SetWidth(90)
+ bindclear:SetColor(Color(200,0,0))
+ bindclear:SetIcon("icon16/keyboard_delete.png")
+ function bindclear:DoClick()
+ binder1:SetSelectedNumber(0)
+ binder2:SetSelectedNumber(0)
+ binder3:SetSelectedNumber(0)
+ send_active_shortcut_to_assign()
+ update_shortcutaction_choices_textCurrentShortcut(shortcutaction_index:GetValue())
+ refresh_shortcut_dumps()
+ end
+
+ local bindoverwrite = vgui.Create("DButton", LeftPanel)
+ bindoverwrite:SetText("confirm")
+ bindoverwrite:SetTooltip("applies the current shortcut combination at the current index")
+ bindoverwrite:SetX(105)
+ bindoverwrite:SetY(480)
+ bindoverwrite:SetHeight(30)
+ bindoverwrite:SetWidth(90)
+ bindoverwrite:SetColor(Color(0,200,0))
+ bindoverwrite:SetIcon("icon16/disk.png")
+ function bindoverwrite:DoClick()
+ local _, action = shortcutaction_choices:GetSelected()
+ local tbl = {}
+ local i = 1
+ --print(binder1:GetValue(), binder2:GetValue(), binder3:GetValue())
+ if binder1:GetValue() ~= 0 then tbl[i] = input.GetKeyName(binder1:GetValue()) i = i + 1 end
+ if binder2:GetValue() ~= 0 then tbl[i] = input.GetKeyName(binder2:GetValue()) i = i + 1 end
+ if binder3:GetValue() ~= 0 then tbl[i] = input.GetKeyName(binder3:GetValue()) end
+ if not table.IsEmpty(tbl) then
+ pace.FlashNotification(action .. " " .. "Combo " .. shortcutaction_index:GetValue() .. " committed: " .. table.concat(tbl," "))
+ if not pace.PACActionShortcut[action] then
+ pace.PACActionShortcut[action] = {}
+ end
+ send_active_shortcut_to_assign(tbl)
+ update_shortcutaction_choices_textCurrentShortcut(shortcutaction_index:GetValue())
+ end
+ encode_table_to_file("pac_editor_shortcuts")
+ end
+
+ function bindoverwrite:DoRightClick()
+ Derma_StringRequest("Save preset", "Save a keyboard shortcuts preset?", "pac_editor_shortcuts",
+ function(name) file.Write("pac3_config/"..name..".txt", util.TableToKeyValues(pace.PACActionShortcut))
+ shortcutaction_presets:AddChoice(name..".txt")
+ end
+ )
+ end
+
+ local bindcapture_text = vgui.Create("DLabel", LeftPanel)
+ bindcapture_text:SetFont("DermaDefaultBold")
+ bindcapture_text:SetText("")
+ bindcapture_text:SetColor(Color(0,0,0))
+ bindcapture_text:SetX(300)
+ bindcapture_text:SetY(480)
+ bindcapture_text:SetSize(300, 30)
+
+ function bindcapture_text:Think()
+ self:SetText(pace.bindcapturelabel_text)
+ end
+ local bindcapture = vgui.Create("DButton", LeftPanel)
+ bindcapture:SetText("capture input")
+ bindcapture:SetX(200)
+ bindcapture:SetY(480)
+ bindcapture:SetHeight(30)
+ bindcapture:SetWidth(90)
+ pace.bindcapturelabel_text = ""
+ function bindcapture:DoClick()
+ local previous_inputs_tbl = {}
+ pace.delayshortcuts = RealTime() + 5
+ local input_active = {}
+ local no_input = true
+ local inputs_str = ""
+ local previous_inputs_str = ""
+ pace.FlashNotification("Recording input... Release one key when you're done")
+
+ pac.AddHook("Tick", "pace_buttoncapture_countdown", function()
+ pace.delayshortcuts = RealTime() + 5
+ local inputs_tbl = {}
+ inputs_str = ""
+ for i=1,172,1 do --build bool list of all current keys
+ if pace.shortcuts_ignored_keys[i] then continue end
+ if input.IsKeyDown(i) then
+ input_active[i] = true
+ inputs_tbl[i] = true
+ no_input = false
+ inputs_str = inputs_str .. input.GetKeyName(i) .. " "
+ else
+ input_active[i] = false
+ end
+ end
+ pace.bindcapturelabel_text = "Recording input:\n" .. inputs_str
+
+ if previous_inputs_tbl and table.Count(previous_inputs_tbl) > 0 then
+ if table.Count(inputs_tbl) < table.Count(previous_inputs_tbl) then
+ pace.FlashNotification("ending input!" .. previous_inputs_str)
+ local tbl = {}
+ local i = 1
+ for key,bool in pairs(previous_inputs_tbl) do
+ tbl[i] = input.GetKeyName(key)
+ i = i + 1
+ end
+ --print(shortcutaction_choices:GetValue(), shortcutaction_index:GetValue())
+ local _, action = shortcutaction_choices:GetSelected()
+ pace.AssignEditorShortcut(action, tbl, shortcutaction_index:GetValue())
+ --pace.PACActionShortcut[shortcutaction_choices:GetValue()][shortcutaction_index:GetValue()] = tbl
+ pace.delayshortcuts = RealTime() + 5
+ pace.bindcapturelabel_text = "Recorded input:\n" .. previous_inputs_str
+ previous_inputs_tbl = {}
+ inputs_tbl = {}
+ pac.RemoveHook("Tick", "pace_buttoncapture_countdown")
+ if #tbl < 4 then
+ if tbl[1] then
+ binder1:SetValue(input.GetKeyCode(tbl[1]))
+ end
+ if tbl[2] then
+ binder2:SetValue(input.GetKeyCode(tbl[2]))
+ end
+ if tbl[3] then
+ binder3:SetValue(input.GetKeyCode(tbl[3]))
+ end
+ end
+ end
+ end
+ previous_inputs_str = inputs_str
+ previous_inputs_tbl = inputs_tbl
+ end)
+
+ end
+
+ local bulkbinder = vgui.Create("DBinder", LeftPanel)
+ function bulkbinder:OnChange( num )
+ if num == 0 then GetConVar("pac_bulk_select_key"):SetString("") return end
+ GetConVar("pac_bulk_select_key"):SetString(input.GetKeyName( num ))
+ end
+ bulkbinder:SetX(210)
+ bulkbinder:SetY(400)
+ bulkbinder:SetSize(80,20)
+ bulkbinder:SetText("bulk select key")
+
+ local function ClearPartMenuPreviewList()
+ local i = 0
+ while (partmenu_previews:GetLine(i + 1) ~= nil) do
+ i = i+1
+ end
+ for v=i,0,-1 do
+ if partmenu_previews:GetLine(v) ~= nil then partmenu_previews:RemoveLine(v) end
+ v = v - 1
+ end
+ end
+
+ local function FindImage(option_name)
+ if option_name == "save" then
+ return pace.MiscIcons.save
+ elseif option_name == "load" then
+ return pace.MiscIcons.load
+ elseif option_name == "wear" then
+ return pace.MiscIcons.wear
+ elseif option_name == "remove" then
+ return pace.MiscIcons.clear
+ elseif option_name == "copy" then
+ return pace.MiscIcons.copy
+ elseif option_name == "paste" then
+ return pace.MiscIcons.paste
+ elseif option_name == "cut" then
+ return "icon16/cut.png"
+ elseif option_name == "paste_properties" then
+ return pace.MiscIcons.replace
+ elseif option_name == "clone" then
+ return pace.MiscIcons.clone
+ elseif option_name == "partsize_info" then
+ return"icon16/drive.png"
+ elseif option_name == "bulk_apply_properties" then
+ return "icon16/application_form.png"
+ elseif option_name == "bulk_select" then
+ return "icon16/table_multiple.png"
+ elseif option_name == "spacer" then
+ return "icon16/application_split.png"
+ elseif option_name == "hide_editor" then
+ return "icon16/application_delete.png"
+ elseif option_name == "expand_all" then
+ return "icon16/arrow_down.png"
+ elseif option_name == "collapse_all" then
+ return "icon16/arrow_in.png"
+ elseif option_name == "copy_uid" then
+ return pace.MiscIcons.uniqueid
+ elseif option_name == "help_part_info" then
+ return "icon16/information.png"
+ elseif option_name == "reorder_movables" then
+ return "icon16/application_double.png"
+ elseif option_name == "criteria_process" then
+ return "icon16/text_list_numbers.png"
+ elseif option_name == "bulk_morph" then
+ return "icon16/chart_line.png"
+ elseif option_name == "arraying_menu" then
+ return "icon16/shape_group.png"
+ elseif option_name == "view_lockon" then
+ return "icon16/zoom.png"
+ elseif option_name == "view_goto" then
+ return "icon16/arrow_turn_right.png"
+ end
+ return "icon16/world.png"
+ end
+
+ partmenu_choices:SetY(50)
+ partmenu_choices:SetX(10)
+ for i,v in pairs(pace.operations_all_operations) do
+ local pnl = vgui.Create("DButton", f)
+ pnl:SetText(string.Replace(string.upper(v),"_"," "))
+ pnl:SetImage(FindImage(v))
+ pnl:SetTooltip("Left click to add at the end\nRight click to insert at the beginning")
+
+ function pnl:DoClick()
+ table.insert(buildlist_partmenu,v)
+ partmenu_previews:AddLine(#buildlist_partmenu,v)
+ end
+ function pnl:DoRightClick()
+ table.insert(buildlist_partmenu,1,v)
+ local previous_list = {}
+ for i,v in ipairs(partmenu_previews:GetLines()) do
+ table.insert(previous_list,v:GetValue(2))
+ end
+ ClearPartMenuPreviewList()
+ partmenu_previews:AddLine(1,v)
+ for i,v in ipairs(previous_list) do
+ partmenu_previews:AddLine(i+1,v)
+ end
+ end
+ partmenu_choices:AddItem(pnl)
+ pnl:SetHeight(18)
+ pnl:SetWidth(200)
+ pnl:SetY(20*(i-1))
+ end
+
+ partmenu_choices:SetWidth(200)
+ partmenu_choices:SetHeight(320)
+ partmenu_choices:SetVerticalScrollbarEnabled(true)
+
+
+ local RightPanel = vgui.Create( "DTree", f )
+ local Test_Node = RightPanel:AddNode( "Test", "icon16/world.png" )
+ local test_part = pac.CreatePart("base") //the menu needs a part to get its full version in preview
+ function RightPanel:DoRightClick()
+ temp_list = pace.operations_order
+ pace.operations_order = buildlist_partmenu
+ pace.OnPartMenu(test_part)
+ temp_list = pace.operations_order
+ pace.operations_order = temp_list
+ end
+ function RightPanel:DoClick()
+ temp_list = pace.operations_order
+ pace.operations_order = buildlist_partmenu
+ pace.OnPartMenu(test_part)
+ temp_list = pace.operations_order
+ pace.operations_order = temp_list
+ end
+ test_part:Remove() //dumb workaround but it works
+
+
+ local div = vgui.Create( "DHorizontalDivider", f )
+ div:Dock( FILL )
+ div:SetLeft( LeftPanel )
+ div:SetRight( RightPanel )
+
+ div:SetDividerWidth( 8 )
+ div:SetLeftMin( 50 )
+ div:SetRightMin( 50 )
+ div:SetLeftWidth( 700 )
+ partmenu_order_presets.OnSelect = function( self, index, value )
+ local temp_list = {"wear","save","load"}
+ if value == "factory preset" then
+ temp_list = table.Copy(pace.operations_default)
+ elseif value == "legacy" then
+ temp_list = table.Copy(pace.operations_legacy)
+ elseif value == "expanded PAC4.5 preset" then
+ temp_list = table.Copy(pace.operations_experimental)
+ elseif value == "bulk select poweruser" then
+ temp_list = table.Copy(pace.operations_bulk_poweruser)
+ elseif value == "user preset" then
+ temp_list = pace.operations_order
+ end
+ ClearPartMenuPreviewList()
+ for i,v in ipairs(temp_list) do
+ partmenu_previews:AddLine(i,v)
+ end
+ buildlist_partmenu = temp_list
+ end
+
+ function partmenu_apply_button:DoClick()
+ pace.operations_order = buildlist_partmenu
+ end
+
+ function partmenu_clearlist_button:DoClick()
+ ClearPartMenuPreviewList()
+ buildlist_partmenu = {}
+ end
+
+ function partmenu_savelist_button:DoClick()
+ encode_table_to_file("pac_editor_partmenu_layouts")
+ end
+
+ function partmenu_previews:DoDoubleClick(id, line)
+ table.remove(buildlist_partmenu,id)
+
+ ClearPartMenuPreviewList()
+ for i,v in ipairs(buildlist_partmenu) do
+ partmenu_previews:AddLine(i,v)
+ end
+
+ PrintTable(buildlist_partmenu)
+ end
+
+
+ if pace.operations_order then
+ for i,v in pairs(pace.operations_order) do
+ table.insert(buildlist_partmenu,v)
+ partmenu_previews:AddLine(#buildlist_partmenu,v)
+ end
+ end
+
+ return f
+end
+
+--camera movement
+function pace.FillEditorSettings2(pnl)
+ local panel = vgui.Create( "DPanel", pnl )
+ --[[ movement binds
+ CreateConVar("pac_editor_camera_forward_bind", "w")
+
+ CreateConVar("pac_editor_camera_back_bind", "s")
+
+ CreateConVar("pac_editor_camera_moveleft_bind", "a")
+
+ CreateConVar("pac_editor_camera_moveright_bind", "d")
+
+ CreateConVar("pac_editor_camera_up_bind", "space")
+
+ CreateConVar("pac_editor_camera_down_bind", "")
+
+ ]]
+
+ --[[pace.camera_movement_binds = {
+ ["forward"] = pace.camera_forward_bind,
+ ["back"] = pace.camera_back_bind,
+ ["moveleft"] = pace.camera_moveleft_bind,
+ ["moveright"] = pace.camera_moveright_bind,
+ ["up"] = pace.camera_up_bind,
+ ["down"] = pace.camera_down_bind,
+ ["slow"] = pace.camera_slow_bind,
+ ["speed"] = pace.camera_speed_bind,
+ ["roll_drag"] = pace.camera_roll_drag_bind
+ }
+ ]]
+
+ local LeftPanel = vgui.Create( "DPanel", panel ) -- Can be any panel, it will be stretched
+ local RightPanel = vgui.Create( "DPanel", panel ) -- Can be any panel, it will be stretched
+ LeftPanel:SetSize(300,600)
+ RightPanel:SetSize(300,600)
+ local div = vgui.Create( "DHorizontalDivider", panel )
+ div:Dock( FILL )
+ div:SetLeft( LeftPanel )
+ div:SetRight( RightPanel )
+
+ div:SetDividerWidth( 8 )
+ div:SetLeftMin( 50 )
+ div:SetRightMin( 50 )
+ div:SetLeftWidth( 400 )
+
+ local movement_binders_label = vgui.Create("DLabel", LeftPanel)
+ movement_binders_label:SetText("PAC editor camera movement")
+ movement_binders_label:SetFont("DermaDefaultBold")
+ movement_binders_label:SetColor(Color(0,0,0))
+ movement_binders_label:SetSize(200,40)
+ movement_binders_label:SetPos(30,5)
+
+ local forward_binder = vgui.Create("DBinder", LeftPanel)
+ forward_binder:SetSize(40,40)
+ forward_binder:SetPos(100,40)
+ forward_binder:SetTooltip("move forward" .. "\nbound to " .. pace.camera_movement_binds["forward"]:GetString())
+ forward_binder:SetValue(input.GetKeyCode(pace.camera_movement_binds["forward"]:GetString()))
+ function forward_binder:OnChange(num)
+ pace.camera_movement_binds["forward"]:SetString(input.GetKeyName( num ))
+ self:SetTooltip("move forward" .. "\nbound to " .. input.GetKeyName( num ))
+ end
+
+ local back_binder = vgui.Create("DBinder", LeftPanel)
+ back_binder:SetSize(40,40)
+ back_binder:SetPos(100,80)
+ back_binder:SetTooltip("move back" .. "\nbound to " .. pace.camera_movement_binds["back"]:GetString())
+ back_binder:SetValue(input.GetKeyCode(pace.camera_movement_binds["back"]:GetString()))
+ function back_binder:OnChange(num)
+ pace.camera_movement_binds["back"]:SetString(input.GetKeyName( num ))
+ self:SetTooltip("move back" .. "\nbound to " .. input.GetKeyName( num ))
+ end
+
+ local moveleft_binder = vgui.Create("DBinder", LeftPanel)
+ moveleft_binder:SetSize(40,40)
+ moveleft_binder:SetPos(60,80)
+ moveleft_binder:SetTooltip("move left" .. "\nbound to " .. pace.camera_movement_binds["moveleft"]:GetString())
+ moveleft_binder:SetValue(input.GetKeyCode(pace.camera_movement_binds["moveleft"]:GetString()))
+ function moveleft_binder:OnChange(num)
+ pace.camera_movement_binds["moveleft"]:SetString(input.GetKeyName( num ))
+ self:SetTooltip("move left" .. "\nbound to " .. input.GetKeyName( num ))
+ end
+
+ local moveright_binder = vgui.Create("DBinder", LeftPanel)
+ moveright_binder:SetSize(40,40)
+ moveright_binder:SetPos(140,80)
+ moveright_binder:SetTooltip("move right" .. "\nbound to " .. pace.camera_movement_binds["moveright"]:GetString())
+ moveright_binder:SetValue(input.GetKeyCode(pace.camera_movement_binds["moveright"]:GetString()))
+ function moveright_binder:OnChange(num)
+ pace.camera_movement_binds["moveright"]:SetString(input.GetKeyName( num ))
+ self:SetTooltip("move right" .. "\nbound to " .. input.GetKeyName( num ))
+ end
+
+ local up_binder = vgui.Create("DBinder", LeftPanel)
+ up_binder:SetSize(40,40)
+ up_binder:SetPos(180,40)
+ up_binder:SetTooltip("move up" .. "\nbound to " .. pace.camera_movement_binds["up"]:GetString())
+ up_binder:SetValue(input.GetKeyCode(pace.camera_movement_binds["up"]:GetString()))
+ function up_binder:OnChange(num)
+ pace.camera_movement_binds["up"]:SetString(input.GetKeyName( num ))
+ self:SetTooltip("move up" .. "\nbound to " .. input.GetKeyName( num ))
+ end
+
+ local down_binder = vgui.Create("DBinder", LeftPanel)
+ down_binder:SetSize(40,40)
+ down_binder:SetPos(180,80)
+ down_binder:SetTooltip("move down" .. "\nbound to " .. pace.camera_movement_binds["down"]:GetString())
+ down_binder:SetValue(input.GetKeyCode(pace.camera_movement_binds["down"]:GetString()))
+ function down_binder:OnChange(num)
+ pace.camera_movement_binds["down"]:SetString(input.GetKeyName( num ))
+ self:SetTooltip("move down" .. "\nbound to " .. input.GetKeyName( num ))
+ end
+
+ local slow_binder = vgui.Create("DBinder", LeftPanel)
+ slow_binder:SetSize(40,40)
+ slow_binder:SetPos(20,80)
+ slow_binder:SetTooltip("go slow" .. "\nbound to " .. pace.camera_movement_binds["slow"]:GetString())
+ slow_binder:SetValue(input.GetKeyCode(pace.camera_movement_binds["slow"]:GetString()))
+ function slow_binder:OnChange(num)
+ pace.camera_movement_binds["slow"]:SetString(input.GetKeyName( num ))
+ self:SetTooltip("go slow" .. "\nbound to " .. input.GetKeyName( num ))
+ end
+
+ local speed_binder = vgui.Create("DBinder", LeftPanel)
+ speed_binder:SetSize(40,40)
+ speed_binder:SetPos(20,40)
+ speed_binder:SetTooltip("go fast" .. "\nbound to " .. pace.camera_movement_binds["speed"]:GetString())
+ speed_binder:SetValue(input.GetKeyCode(pace.camera_movement_binds["speed"]:GetString()))
+ function speed_binder:OnChange(num)
+ pace.camera_movement_binds["speed"]:SetString(input.GetKeyName( num ))
+ self:SetTooltip("go fast" .. "\nbound to " .. input.GetKeyName( num ))
+ end
+
+ local roll_binder = vgui.Create("DBinder", LeftPanel)
+ roll_binder:SetSize(40,40)
+ roll_binder:SetPos(60,40)
+ roll_binder:SetTooltip("roll drag (hold & drag to tilt, tap to reset)" .. "\nbound to " .. pace.camera_movement_binds["roll_drag"]:GetString())
+ roll_binder:SetValue(input.GetKeyCode(pace.camera_movement_binds["roll_drag"]:GetString()))
+ function roll_binder:OnChange(num)
+ pace.camera_movement_binds["roll_drag"]:SetString(input.GetKeyName( num ))
+ self:SetTooltip("roll drag (hold & drag to tilt, tap to reset)" .. "\nbound to " .. input.GetKeyName( num ))
+ end
+
+ local Parts = pac.GetRegisteredParts()
+ local function get_icon(str, fallback)
+ if str then
+ if pace.MiscIcons[string.gsub(str, "pace.MiscIcons.", "")] then
+ return pace.MiscIcons[string.gsub(str, "pace.MiscIcons.", "")]
+ else
+ local img = string.gsub(str, ".png", "") --remove the png extension
+ img = string.gsub(img, "icon16/", "") --remove the icon16 base path
+ img = "icon16/" .. img .. ".png" --why do this? to be able to write any form and let the program fix the form
+ return img
+ end
+ elseif Parts[fallback] then
+ return Parts[fallback].Icon
+ else
+ return "icon16/page_white.png"
+ end
+
+ end
+
+ local categorytree = vgui.Create("DTree", RightPanel)
+ categorytree:SetY(30)
+ categorytree:SetSize(360,400)
+
+ local function class_partnode_add(parentnode, class)
+ if Parts[class] then
+ for i,v in ipairs(parentnode:GetChildNodes()) do --can't make duplicates so remove to place it at the end
+ if v:GetText() == class then v:Remove() end
+ end
+
+ local part_node = parentnode:AddNode(class)
+ part_node:SetIcon(get_icon(nil, class))
+ part_node.DoRightClick = function()
+ local menu = DermaMenu()
+ menu:AddOption("remove", function() part_node:Remove() end):SetImage("icon16/cross.png")
+ menu:MakePopup()
+ menu:SetPos(input.GetCursorPos())
+ end
+ end
+ end
+ local function bring_up_partclass_list(cat_node)
+
+ --function from pace.OnAddPartMenu(obj)
+ local base = vgui.Create("EditablePanel")
+ base:SetPos(input.GetCursorPos())
+ base:SetSize(200, 300)
+
+ base:MakePopup()
+
+ function base:OnRemove()
+ pac.RemoveHook("VGUIMousePressed", "search_part_menu")
+ end
+
+ local edit = base:Add("DTextEntry")
+ edit:SetTall(20)
+ edit:Dock(TOP)
+ edit:RequestFocus()
+ edit:SetUpdateOnType(true)
+
+ local result = base:Add("DScrollPanel")
+ result:Dock(FILL)
+
+ function edit:OnEnter()
+ if result.found[1] then
+ class_partnode_add(cat_node, result.found[1].ClassName)
+ end
+ base:Remove()
+ end
+
+ edit.OnValueChange = function(_, str)
+ result:Clear()
+ result.found = {}
+
+ for _, part in ipairs(pace.GetRegisteredParts()) do
+ if (part.FriendlyName or part.ClassName):find(str, nil, true) then
+ table.insert(result.found, part)
+ end
+ end
+
+ table.sort(result.found, function(a, b) return #a.ClassName < #b.ClassName end)
+
+ for _, part in ipairs(result.found) do
+ local line = result:Add("DButton")
+ line:SetText("")
+ line:SetTall(20)
+ line.DoClick = function()
+ class_partnode_add(cat_node, part.ClassName)
+ end
+
+ local btn = line:Add("DImageButton")
+ btn:SetSize(16, 16)
+ btn:SetPos(4,0)
+ btn:CenterVertical()
+ btn:SetMouseInputEnabled(false)
+ if part.Icon then
+ btn:SetImage(part.Icon)
+ end
+
+ local label = line:Add("DLabel")
+ label:SetTextColor(label:GetSkin().Colours.Category.Line.Text)
+ label:SetText((part.FriendlyName or part.ClassName):Replace('_', ' '))
+ label:SizeToContents()
+ label:MoveRightOf(btn, 4)
+ label:SetMouseInputEnabled(false)
+ label:CenterVertical()
+
+ line:Dock(TOP)
+ end
+
+ --base:SetHeight(20 * #result.found + edit:GetTall())
+ base:SetHeight(600 + edit:GetTall())
+
+ end
+
+ edit:OnValueChange("")
+
+ pac.AddHook("VGUIMousePressed", "search_part_menu", function(pnl, code)
+ if code == MOUSE_LEFT or code == MOUSE_RIGHT then
+ if not base:IsOurChild(pnl) then
+ base:Remove()
+ end
+ end
+ end)
+ end
+ local function bring_up_category_icon_browser(category_node)
+ local master_frame = vgui.Create("DFrame")
+ master_frame:SetPos(input.GetCursorPos())
+ master_frame:SetSize(400,400)
+
+ local browser = vgui.Create("DIconBrowser", master_frame)
+ function browser:OnChange()
+ category_node:SetIcon(self:GetSelectedIcon())
+ end
+ browser:SetSize(400,380)
+ browser:SetPos(0,40)
+
+ local frame = vgui.Create("EditablePanel", master_frame)
+ local edit = vgui.Create("DTextEntry", frame)
+ frame:SetSize(300,20)
+ function browser:Think()
+ if not IsValid(category_node) then master_frame:Remove() end
+ x = master_frame:GetX()
+ y = master_frame:GetY()
+ frame:SetPos(x,y+20)
+ frame:MakePopup()
+ end
+
+ function edit:OnValueChange(value)
+ browser:FilterByText( value )
+ end
+ master_frame:MakePopup()
+ frame:MakePopup()
+ edit:Dock(TOP)
+ edit:RequestFocus()
+ edit:SetUpdateOnType(true)
+ end
+ local function bring_up_tooltip_edit(category_node)
+ local frame = vgui.Create("EditablePanel")
+ local edit = vgui.Create("DTextEntry", frame)
+ function edit:OnEnter(value)
+ category_node:SetTooltip(value)
+ frame:Remove()
+ end
+ function frame:Think()
+ if input.IsMouseDown(MOUSE_LEFT) and not (self:IsHovered() or edit:IsHovered()) then self:Remove() end
+ end
+ frame:MakePopup()
+
+ frame:SetSize(300,30)
+ frame:SetPos(input.GetCursorPos())
+
+ edit:Dock(TOP)
+ edit:RequestFocus()
+ edit:SetUpdateOnType(true)
+ end
+ local function bring_up_name_edit(category_node)
+ local frame = vgui.Create("EditablePanel")
+ local edit = vgui.Create("DTextEntry", frame)
+ edit:SetText(category_node:GetText())
+ function edit:OnEnter(value)
+ category_node:SetText(value)
+ frame:Remove()
+ end
+ function frame:Think()
+ if input.IsMouseDown(MOUSE_LEFT) and not (self:IsHovered() or edit:IsHovered()) then self:Remove() end
+ end
+ frame:MakePopup()
+
+ frame:SetSize(300,30)
+ frame:SetPos(category_node.Label:LocalToScreen(category_node.Label:GetPos()))
+
+ edit:Dock(TOP)
+ edit:RequestFocus()
+ edit:SetUpdateOnType(true)
+ end
+
+ local function load_partgroup_template_into_tree(categorytree, tbl)
+ tbl = tbl or pace.partgroups or pace.partmenu_categories_default
+ categorytree:Clear()
+ for category,category_contents in pairs(tbl) do
+
+ local category_node = categorytree:AddNode(category)
+ category_node:SetIcon(get_icon(category_contents.icon, category))
+
+ category_node.DoRightClick = function()
+ local menu = DermaMenu()
+ menu:AddOption("insert part in category", function() bring_up_partclass_list(category_node) end):SetImage("icon16/add.png")
+ menu:AddOption("select icon", function() bring_up_category_icon_browser(category_node) end):SetImage("icon16/picture.png")
+ menu:AddOption("write a tooltip", function() bring_up_tooltip_edit(category_node) end):SetImage("icon16/comment.png")
+ menu:AddOption("rename this category", function() bring_up_name_edit(category_node) end):SetImage("icon16/textfield_rename.png")
+ menu:AddOption("remove this category", function() category_node:Remove() end):SetImage("icon16/cross.png")
+ menu:MakePopup()
+ menu:SetPos(input.GetCursorPos())
+ end
+
+ if category_contents["tooltip"] then
+ category_node:SetTooltip(category_contents["tooltip"])
+ end
+
+ for field,value in pairs(category_contents) do
+ if Parts[field] then
+ class_partnode_add(category_node, field)
+ end
+ end
+ end
+ end
+
+ local function extract_partgroup_template_from_tree(categorytree)
+ local tbl = {}
+ for i,category_node in ipairs(categorytree:Root():GetChildNodes()) do
+ tbl[category_node:GetText()] = {}
+ --print(i,category_node:GetText(),category_node.Label:GetTooltip(), category_node:GetIcon())
+ if category_node:GetTooltip() ~= nil and category_node:GetTooltip() ~= "" then tbl[category_node:GetText()]["tooltip"] = category_node:GetTooltip() end
+ tbl[category_node:GetText()]["icon"] = category_node:GetIcon()
+
+ for i2,part_node in ipairs(category_node:GetChildNodes()) do
+ tbl[category_node:GetText()][part_node:GetText()] = part_node:GetText()
+ --print("\t",part_node:GetText())
+ end
+ end
+ return tbl
+ end
+
+ load_partgroup_template_into_tree(categorytree, pace.partgroups)
+
+ local part_categories_presets = vgui.Create("DComboBox", RightPanel)
+ part_categories_presets:SetText("Select a part category preset")
+ part_categories_presets:AddChoice("active preset")
+ part_categories_presets:AddChoice("factory preset")
+ part_categories_presets:AddChoice("experimental preset")
+ local default_partgroup_presets = {
+ ["pac_part_categories.txt"] = true,
+ ["pac_part_categories_experimental.txt"] = true,
+ ["pac_part_categories_default.txt"] = true
+ }
+ for i,filename in ipairs(file.Find("pac3_config/pac_part_categories*.txt","DATA")) do
+ if not default_partgroup_presets[string.GetFileFromFilename(filename)] then
+ part_categories_presets:AddChoice(string.GetFileFromFilename(filename))
+ end
+ end
+
+ part_categories_presets:SetX(10) part_categories_presets:SetY(10)
+ part_categories_presets:SetWidth(170)
+ part_categories_presets:SetHeight(20)
+
+ part_categories_presets.OnSelect = function( self, index, value )
+ if value == "factory preset" then
+ pace.partgroups = pace.partmenu_categories_default
+ elseif value == "experimental preset" then
+ pace.partgroups = pace.partmenu_categories_experimental
+ elseif string.find(value, ".txt") then
+ pace.partgroups = util.KeyValuesToTable(file.Read("pac3_config/"..value))
+ elseif value == "active preset" then
+ decode_table_from_file("pac_part_categories")
+ if not pace.partgroups_user then pace.partgroups_user = pace.partgroups end
+ file.Write("pac3_config/pac_part_categories_user.txt", util.TableToKeyValues(pace.partgroups_user))
+ end
+ load_partgroup_template_into_tree(categorytree, pace.partgroups)
+ end
+
+ local part_categories_save = vgui.Create("DButton", RightPanel)
+ part_categories_save:SetText("Save")
+ part_categories_save:SetImage("icon16/disk.png")
+ part_categories_save:SetX(180) part_categories_save:SetY(10)
+ part_categories_save:SetWidth(80)
+ part_categories_save:SetHeight(20)
+ part_categories_save:SetTooltip("Left click to save preset to the active slot\nRight click to save to a new file")
+ part_categories_save.DoClick = function()
+ pace.partgroups = extract_partgroup_template_from_tree(categorytree)
+ file.Write("pac3_config/pac_part_categories.txt", util.TableToKeyValues(extract_partgroup_template_from_tree(categorytree)))
+ end
+ part_categories_save.DoRightClick = function()
+ Derma_StringRequest("Save preset", "Save a part category preset?", "pac_part_categories",
+ function(name) file.Write("pac3_config/"..name..".txt", util.TableToKeyValues(extract_partgroup_template_from_tree(categorytree)))
+ part_categories_presets:AddChoice(name..".txt")
+ end
+ )
+ end
+
+ local part_categories_add_cat = vgui.Create("DButton", RightPanel)
+ part_categories_add_cat:SetText("Add category")
+ part_categories_add_cat:SetImage("icon16/add.png")
+ part_categories_add_cat:SetX(260) part_categories_add_cat:SetY(10)
+ part_categories_add_cat:SetWidth(100)
+ part_categories_add_cat:SetHeight(20)
+ part_categories_add_cat.DoClick = function()
+ local category_node = categorytree:AddNode("Category " .. categorytree:Root():GetChildNodeCount() + 1)
+ category_node:SetIcon("icon16/page_white.png")
+
+ category_node.DoRightClick = function()
+ local menu = DermaMenu()
+ menu:AddOption("insert part in category", function() bring_up_partclass_list(category_node) end):SetImage("icon16/add.png")
+ menu:AddOption("select icon", function() bring_up_category_icon_browser(category_node) end):SetImage("icon16/picture.png")
+ menu:AddOption("write a tooltip", function() bring_up_tooltip_edit(category_node) end):SetImage("icon16/comment.png")
+ menu:AddOption("rename this category", function() bring_up_name_edit(category_node) end):SetImage("icon16/textfield_rename.png")
+ menu:AddOption("remove this category", function() category_node:Remove() end):SetImage("icon16/cross.png")
+ menu:MakePopup()
+ menu:SetPos(input.GetCursorPos())
+ end
+ end
+
+ return panel
+end
+
+function pace.GetPartMenuComponentPreviewForMenuEdit(menu, option_name)
+ local pnl = vgui.Create("DButton", menu)
+ pnl:SetText(string.Replace(string.upper(option_name),"_"," "))
+ return pnl
+end
+
+function pace.ConfigureEventWheelMenu()
+ pace.command_colors = pace.command_colors or {}
+ local master_panel = vgui.Create("DFrame")
+ master_panel:SetTitle("event wheel config")
+ master_panel:SetSize(500,800)
+ master_panel:Center()
+ local mid_panel = vgui.Create("DPanel", master_panel)
+ mid_panel:Dock(FILL)
+
+ local scr_pnl = vgui.Create("DScrollPanel", mid_panel)
+ scr_pnl:Dock(FILL)
+ scr_pnl:SetPos(0,45)
+ local list = vgui.Create("DListLayout", scr_pnl) list:Dock(FILL)
+
+ local first_panel = vgui.Create("DPanel", mid_panel)
+ first_panel:SetSize(500,40)
+ first_panel:Dock(TOP)
+
+ local circle_style_listmenu = vgui.Create("DComboBox",first_panel)
+ circle_style_listmenu:SetText("Choose eventwheel style")
+ circle_style_listmenu:SetSize(150,20)
+ circle_style_listmenu:AddChoice("legacy")
+ circle_style_listmenu:AddChoice("concentric")
+ circle_style_listmenu:AddChoice("alternative")
+ function circle_style_listmenu:OnSelect( index, value )
+ if value == "legacy" then
+ GetConVar("pac_eventwheel_style"):SetString("0")
+ elseif value == "concentric" then
+ GetConVar("pac_eventwheel_style"):SetString("1")
+ elseif value == "alternative" then
+ GetConVar("pac_eventwheel_style"):SetString("2")
+ end
+ end
+
+ local circle_clickmode = vgui.Create("DComboBox",first_panel)
+ circle_clickmode:SetText("Choose eventwheel clickmode")
+ circle_clickmode:SetSize(160,20)
+ circle_clickmode:SetPos(150,0)
+ circle_clickmode:AddChoice("clickable and activates on close")
+ circle_clickmode:AddChoice("not clickable, but activate on close")
+ circle_clickmode:AddChoice("clickable, but do not activate on close")
+ function circle_clickmode:OnSelect( index, value )
+ if value == "clickable and activates on close" then
+ GetConVar("pac_eventwheel_clickmode"):SetString("0")
+ elseif value == "not clickable, but activate on close" then
+ GetConVar("pac_eventwheel_clickmode"):SetString("-1")
+ elseif value == "clickable, but do not activate on close" then
+ GetConVar("pac_eventwheel_clickmode"):SetString("1")
+ end
+ end
+
+
+ local rectangle_style_listmenu = vgui.Create("DComboBox",first_panel)
+ rectangle_style_listmenu:SetText("Choose eventlist style")
+ rectangle_style_listmenu:SetSize(150,20)
+ rectangle_style_listmenu:SetPos(0,20)
+ rectangle_style_listmenu:AddChoice("legacy-like")
+ rectangle_style_listmenu:AddChoice("concentric")
+ rectangle_style_listmenu:AddChoice("alternative")
+
+ function rectangle_style_listmenu:OnSelect( index, value )
+ if value == "legacy-like" then
+ GetConVar("pac_eventlist_style"):SetString("0")
+ elseif value == "concentric" then
+ GetConVar("pac_eventlist_style"):SetString("1")
+ elseif value == "alternative" then
+ GetConVar("pac_eventlist_style"):SetString("2")
+ end
+ end
+
+ local rectangle_clickmode = vgui.Create("DComboBox",first_panel)
+ rectangle_clickmode:SetText("Choose eventlist clickmode")
+ rectangle_clickmode:SetSize(160,20)
+ rectangle_clickmode:SetPos(150,20)
+ rectangle_clickmode:AddChoice("clickable and activates on close")
+ rectangle_clickmode:AddChoice("not clickable, but activate on close")
+ rectangle_clickmode:AddChoice("clickable, but do not activate on close")
+ function rectangle_clickmode:OnSelect( index, value )
+ if value == "clickable and activates on close" then
+ GetConVar("pac_eventlist_clickmode"):SetString("0")
+ elseif value == "not clickable, but activate on close" then
+ GetConVar("pac_eventlist_clickmode"):SetString("-1")
+ elseif value == "clickable, but do not activate on close" then
+ GetConVar("pac_eventlist_clickmode"):SetString("1")
+ end
+ end
+
+ local rectangle_fontsize = vgui.Create("DComboBox",first_panel)
+ rectangle_fontsize:SetText("Eventlist font / height")
+ rectangle_fontsize:SetSize(160,20)
+ rectangle_fontsize:SetPos(310,20)
+ for _,font in ipairs(pace.Fonts) do
+ rectangle_fontsize:AddChoice(font)
+ end
+
+ function rectangle_fontsize:OnSelect( index, value )
+ GetConVar("pac_eventlist_font"):SetString(value)
+ end
+
+ local circle_fontsize = vgui.Create("DComboBox",first_panel)
+ circle_fontsize:SetText("Eventwheel font size")
+ circle_fontsize:SetSize(160,20)
+ circle_fontsize:SetPos(310,0)
+ for _,font in ipairs(pace.Fonts) do
+ circle_fontsize:AddChoice(font)
+ end
+
+ function circle_fontsize:OnSelect( index, value )
+ GetConVar("pac_eventwheel_font"):SetString(value)
+ end
+
+ local customizer_button_box = vgui.Create("DCheckBox",first_panel)
+ customizer_button_box:SetTooltip("Show the Customize button when eventwheels are active")
+ customizer_button_box:SetSize(20,20)
+ customizer_button_box:SetPos(470,0)
+ customizer_button_box:SetConVar("pac_eventwheel_show_customize_button")
+
+
+ local events = {}
+ for i,v in pairs(pac.GetLocalParts()) do
+ if v.ClassName == "event" then
+ local e = v:GetEvent()
+ if e == "command" then
+ local cmd, time, hide = v:GetParsedArgumentsForObject(v.Events.command)
+ local this_event_hidden = v:IsHiddenBySomethingElse(false)
+ events[cmd] = cmd
+ end
+ end
+
+ end
+
+ local names = table.GetKeys( events )
+ table.sort(names, function(a, b) return a < b end)
+
+ local copied_color = nil
+ local lanes = {}
+ local colorpanel
+ if LocalPlayer().pac_command_events then
+ if table.Count(names) == 0 then
+ local error_label = vgui.Create("DLabel", list)
+ error_label:SetText("Uh oh, nothing to see here! Looks like you don't have any command events in your outfit!\nPlease go back to the editor.")
+ error_label:SetPos(100,200)
+ error_label:SetFont("DermaDefaultBold")
+ error_label:SetSize(450,50)
+ error_label:SetColor(Color(150,0,0))
+ end
+ for _, name in ipairs(names) do
+ local pnl = vgui.Create("DPanel") list:Add(pnl) pnl:SetSize(400,20)
+ local btn = vgui.Create("DButton", pnl)
+
+ btn:SetSize(200,25)
+ btn:SetText(name)
+ btn:SetTooltip(name)
+
+
+ if pace.command_colors[name] then
+ local tbl = string.Split(pace.command_colors[name], " ")
+ btn:SetColor(Color(tonumber(tbl[1]),tonumber(tbl[2]),tonumber(tbl[3])))
+ end
+ local colorbutton = vgui.Create("DButton", pnl)
+ colorbutton:SetText("Color")
+ colorbutton:SetIcon("icon16/color_wheel.png")
+ colorbutton:SetPos(200,0) colorbutton:SetSize(65,20)
+ function colorbutton:DoClick()
+ if IsValid(colorpanel) then colorpanel:Remove() end
+ local clr_frame = vgui.Create("DPanel")
+ colorpanel = clr_frame
+ function clr_frame:Think()
+ if not pace.command_event_menu_opened and not IsValid(master_panel) then self:Remove() end
+ end
+
+ local clr_pnl = vgui.Create("DColorMixer", clr_frame)
+ if pace.command_colors[name] then
+ local str_tbl = string.Split(pace.command_colors[name], " ")
+ clr_pnl:SetBaseColor(Color(tonumber(str_tbl[1]),tonumber(str_tbl[2]),tonumber(str_tbl[3])))
+ end
+
+ clr_frame:SetSize(300,200) clr_pnl:Dock(FILL)
+ clr_frame:SetPos(self:LocalToScreen(0,0))
+ clr_frame:RequestFocus()
+ function clr_pnl:Think()
+ if input.IsMouseDown(MOUSE_LEFT) then
+
+ if not IsValid(vgui.GetHoveredPanel()) then
+ self:Remove() clr_frame:Remove()
+ else
+ if vgui.GetHoveredPanel():GetClassName() == "CGModBase" and not self.clicking then
+ self:Remove() clr_frame:Remove()
+ end
+ end
+ self.clicking = true
+ else
+ self.clicking = false
+ end
+ end
+ function clr_pnl:ValueChanged(col)
+ pace.command_colors = pace.command_colors or {}
+ pace.command_colors[name] = col.r .. " " .. col.g .. " " .. col.b
+ btn:SetColor(col)
+ end
+
+ end
+
+ local copypastebutton = vgui.Create("DButton", pnl)
+ copypastebutton:SetText("Copy/Paste")
+ copypastebutton:SetToolTip("right click to copy\nleft click to paste")
+ copypastebutton:SetIcon("icon16/page_copy.png")
+ copypastebutton:SetPos(265,0) copypastebutton:SetSize(150,20)
+ function copypastebutton:DoClick()
+ if not copied_color then return end
+ pace.command_colors[name] = copied_color
+ btn:SetColor(Color(tonumber(string.Split(copied_color, " ")[1]), tonumber(string.Split(copied_color, " ")[2]), tonumber(string.Split(copied_color, " ")[3])))
+ end
+ function copypastebutton:DoRightClick()
+ for _,tbl in pairs(lanes) do
+ if tbl.cmd ~= name then
+ tbl.copypaste:SetText("Copy/Paste")
+ end
+ end
+ copied_color = pace.command_colors[name]
+ if copied_color then
+ self:SetText("copied: " .. pace.command_colors[name])
+ else
+ self:SetText("no color to copy!")
+ end
+ end
+
+ local clearbutton = vgui.Create("DButton", pnl)
+ clearbutton:SetText("Clear")
+ clearbutton:SetIcon("icon16/cross.png")
+ clearbutton:SetPos(415,0) clearbutton:SetSize(60,20)
+ function clearbutton:DoClick()
+ btn:SetColor(Color(0,0,0))
+ pace.command_colors[name] = nil
+ end
+
+ lanes[name] = {cmd = name, main_btn = btn, color_btn = colorbutton, copypaste = copypastebutton, clear = clearbutton}
+ end
+ end
+
+ function master_panel:OnRemove()
+ gui.EnableScreenClicker(false)
+ pace.command_event_menu_opened = nil
+ encode_table_to_file("eventwheel_colors", pace.command_colors)
+ if pace.event_wheel_list_opened then pac.closeEventSelectionList(true) end
+ if pace.event_wheel_opened then pac.closeEventSelectionWheel(true) end
+ end
+
+ master_panel:RequestFocus()
+ gui.EnableScreenClicker(true)
+ pace.command_event_menu_opened = master_panel
+end
+
+
+decode_table_from_file("pac_editor_shortcuts")
+decode_table_from_file("pac_editor_partmenu_layouts")
+decode_table_from_file("eventwheel_colors")
+
+if not file.Exists("pac_part_categories_experimental.txt", "DATA") then
+ file.Write("pac3_config/pac_part_categories_experimental.txt", util.TableToKeyValues(pace.partmenu_categories_experimental))
+end
+if not file.Exists("pac_part_categories_default.txt", "DATA") then
+ file.Write("pac3_config/pac_part_categories_default.txt", util.TableToKeyValues(pace.partmenu_categories_default))
+end
+decode_table_from_file("pac_part_categories")
+pace.partgroups = pace.partgroups or pace.partmenu_categories_default
diff --git a/lua/pac3/editor/client/shortcuts.lua b/lua/pac3/editor/client/shortcuts.lua
index 40efcff14..8cf1892fc 100644
--- a/lua/pac3/editor/client/shortcuts.lua
+++ b/lua/pac3/editor/client/shortcuts.lua
@@ -1,3 +1,323 @@
+include("parts.lua")
+include("popups_part_tutorials.lua")
+
+local L = pace.LanguageString
+
+concommand.Add( "pac_toggle_focus", function() pace.Call("ToggleFocus") end)
+concommand.Add( "pac_focus", function() pace.Call("ToggleFocus") end)
+
+local legacy_input = CreateConVar("pac_editor_shortcuts_legacy_mode", "1", FCVAR_ARCHIVE, "Reverts the editor to hardcoded key checks ignoring customizable keys. Some keys are hidden and held down causing serious editor usage problems.")
+
+local last_recorded_combination
+
+pace.PACActionShortcut_Dictionary = {
+ "wear",
+ "save",
+ "load",
+ "hide_editor",
+ "hide_editor_visible",
+ "copy",
+ "paste",
+ "cut",
+ "clone",
+ "delete",
+ "expand_all",
+ "collapse_all",
+ "editor_up",
+ "editor_down",
+ "editor_pageup",
+ "editor_pagedown",
+ "editor_node_collapse",
+ "editor_node_expand",
+ "undo",
+ "redo",
+ "hide",
+ "panic",
+ "restart",
+ "partmenu",
+ "add_part",
+ "property_search_current_part",
+ "property_search_in_tree",
+ "toolbar_pac",
+ "toolbar_tools",
+ "toolbar_player",
+ "toolbar_view",
+ "toolbar_options",
+ "zoom_panel",
+ "reset_zoom",
+ "reset_view_position",
+ "view_orthographic",
+ "view_follow_entity",
+ "view_follow_entity_ang_frontback",
+ "view_follow_entity_sideview",
+ "reset_eyeang",
+ "reset_eyeang_pitch",
+ "T_Pose",
+ "bulk_select",
+ "clear_bulkselect",
+ "copy_bulkselect",
+ "bulk_insert",
+ "bulk_delete",
+ "bulk_pack",
+ "bulk_paste_1",
+ "bulk_paste_2",
+ "bulk_paste_3",
+ "bulk_paste_4",
+ "bulk_paste_properties_1",
+ "bulk_paste_properties_2",
+ "bulk_hide",
+ "help_info_popup",
+ "ultra_cleanup",
+ "arraying_menu",
+ "bulk_morph",
+ "criteria_process"
+}
+
+pace.PACActionShortcut_Default = {
+ ["wear"] = {
+ [1] = {"CTRL", "n"}
+ },
+
+ ["save"] = {
+ [1] = {"CTRL", "s"}
+ },
+
+ ["hide_editor"] = {
+ [1] = {"CTRL", "e"}
+ },
+
+ ["help_info_popup"] = {
+ [1] = {"F1"}
+ },
+
+ ["hide_editor_visible"] = {
+ [1] = {"ALT", "e"}
+ },
+
+ ["copy"] = {
+ [1] = {"CTRL", "c"}
+ },
+
+ ["paste"] = {
+ [1] = {"CTRL", "v"}
+ },
+ ["cut"] = {
+ [1] = {"CTRL", "x"}
+ },
+ ["delete"] = {
+ [1] = {"DEL"}
+ },
+ ["expand_all"] = {
+
+ },
+ ["collapse_all"] = {
+
+ },
+ ["undo"] = {
+ [1] = {"CTRL", "z"}
+ },
+ ["redo"] = {
+ [1] = {"CTRL", "y"}
+ },
+ ["T_Pose"] = {
+ [1] = {"CTRL", "t"}
+ },
+ ["property_search_current_part"] = {
+ [1] = {"CTRL", "f"}
+ },
+ ["property_search_in_tree"] = {
+ [1] = {"CTRL", "SHIFT", "f"}
+ },
+ ["editor_up"] = {
+ [1] = {"UPARROW"}
+ },
+ ["editor_down"] = {
+ [1] = {"DOWNARROW"}
+ },
+ ["editor_pageup"] = {
+ [1] = {"PGUP"}
+ },
+ ["editor_pagedown"] = {
+ [1] = {"PGDN"}
+ },
+ ["editor_node_collapse"] = {
+ [1] = {"LEFTARROW"}
+ },
+ ["editor_node_expand"] = {
+ [1] = {"RIGHTARROW"}
+ }
+}
+
+pace.PACActionShortcut_NoCTRL = {
+ ["wear"] = {
+ [1] = {"n"}
+ },
+
+ ["save"] = {
+ [1] = {"m"}
+ },
+
+ ["hide_editor"] = {
+ [1] = {"q"}
+ },
+
+ ["copy"] = {
+ [1] = {"c"}
+ },
+
+ ["paste"] = {
+ [1] = {"v"}
+ },
+
+ ["cut"] = {
+ [1] = {"x"}
+ },
+
+ ["delete"] = {
+ [1] = {"DEL"}
+ },
+
+ ["undo"] = {
+ [1] = {"z"}
+ },
+
+ ["redo"] = {
+ [1] = {"y"}
+ },
+
+ ["T_Pose"] = {
+ [1] = {"t"}
+ }
+}
+
+pace.PACActionShortcut_Experimental = {
+ ["help_info_popup"] = {
+ [1] = {"F1"}
+ },
+ ["property_search_in_tree"] = {
+ [1] = {"CTRL", "f"}
+ },
+ ["wear"] = {
+ [1] = {"CTRL", "n"}
+ },
+ ["restart"] = {
+ [1] = {"CTRL", "ALT", "SHIFT", "r"}
+ },
+ ["panic"] = {
+ [1] = {"CTRL", "ALT", "SHIFT", "p"}
+ },
+
+ ["save"] = {
+ [1] = {"CTRL", "m"}
+ },
+
+ ["load"] = {
+ [1] = {"SHIFT", "m"}
+ },
+
+ ["hide_editor"] = {
+ [1] = {"CTRL", "e"},
+ [2] = {"INS"},
+ [3] = {"TAB"},
+ [4] = {"4"}
+ },
+
+ ["hide_editor_visible"] = {
+ [1] = {"ALT", "e"},
+ [2] = {"5"}
+ },
+
+ ["copy"] = {
+ [1] = {"CTRL", "c"}
+ },
+
+ ["copy_bulkselect"] = {
+ [1] = {"SHIFT", "CTRL", "c"}
+ },
+
+ ["paste"] = {
+ [1] = {"CTRL", "v"}
+ },
+ ["cut"] = {
+ [1] = {"CTRL", "x"}
+ },
+ ["bulk_insert"] = {
+ [1] = {"CTRL", "SHIFT", "v"}
+ },
+ ["delete"] = {
+ [1] = {"DEL"}
+ },
+ ["bulk_delete"] = {
+ [1] = {"SHIFT", "DEL"}
+ },
+ ["clear_bulkselect"] = {
+ [1] = {"CTRL", "SHIFT", "DEL"}
+ },
+ ["undo"] = {
+ [1] = {"CTRL", "z"},
+ [2] = {"u"}
+ },
+ ["redo"] = {
+ [1] = {"CTRL", "y"},
+ [2] = {"i"}
+ },
+ ["T_Pose"] = {
+ [1] = {"CTRL", "t"}
+ },
+ ["zoom_panel"] = {
+ [1] = {"ALT", "v"}
+ },
+ ["toolbar_view"] = {
+ [1] = {"SHIFT", "v"}
+ },
+ ["add_part"] = {
+ [1] = {"1"}
+ },
+ ["partmenu"] = {
+ [1] = {"2"}
+ },
+ ["bulk_select"] = {
+ [1] = {"3"}
+ },
+ ["hide"] = {
+ [1] = {"CTRL", "h"}
+ },
+ ["bulk_hide"] = {
+ [1] = {"SHIFT", "h"}
+ },
+ ["editor_up"] = {
+ [1] = {"UPARROW"}
+ },
+ ["editor_down"] = {
+ [1] = {"DOWNARROW"}
+ },
+ ["editor_pageup"] = {
+ [1] = {"PGUP"}
+ },
+ ["editor_pagedown"] = {
+ [1] = {"PGDN"}
+ },
+ ["editor_node_collapse"] = {
+ [1] = {"LEFTARROW"}
+ },
+ ["editor_node_expand"] = {
+ [1] = {"RIGHTARROW"}
+ }
+}
+
+pace.PACActionShortcut = pace.PACActionShortcut or pace.PACActionShortcut_Experimental
+
+--pace.PACActionShortcut = pace.PACActionShortcuts_NoCTRL
+
+
+ --[[thinkUndo()
+ thinkCopy()
+ thinkPaste()
+ thinkCut()
+ thinkDelete()
+ thinkExpandAll()
+ thinkCollapseAll()]]--
+
function pace.OnShortcutSave()
if not IsValid(pace.current_part) then return end
@@ -17,54 +337,636 @@ function pace.OnShortcutWear()
end
local last = 0
+pace.passthrough_keys = {
+ [KEY_LWIN] = true,
+ [KEY_RWIN] = true,
+ [KEY_CAPSLOCK] = true
+}
+pace.shortcuts_ignored_keys = {
+ [KEY_CAPSLOCKTOGGLE] = true,
+ [KEY_NUMLOCKTOGGLE] = true,
+ [KEY_SCROLLLOCKTOGGLE] = true
+}
+
+function pace.LookupShortcutsForAction(action, provided_inputs, do_it)
+ pace.BulkSelectKey = input.GetKeyCode(GetConVar("pac_bulk_select_key"):GetString())
+
+ --combo is the table of key names for one combo slot
+ local function input_contains_one_match(combo, action, inputs)
+ --if pace.shortcut_inputs_count ~= #combo then return false end --if input has too much or too little keys, we already know it doesn't match
+ for _,key in ipairs(combo) do --check the combo's keys for a match
+ --[[if not (input.IsKeyDown(input.GetKeyCode(key)) and inputs[input.GetKeyCode(key)]) then --all keys must be there
+ return false
+ end]]
+
+ if not input.IsKeyDown(input.GetKeyCode(key)) then --all keys must be there
+ return false
+ end
+ end
+ return true
+ end
-function pace.CheckShortcuts()
- if gui.IsConsoleVisible() then return end
- if not pace.Editor or not pace.Editor:IsValid() then return end
- if last > RealTime() or input.IsMouseDown(MOUSE_LEFT) then return end
+ local function shortcut_contains_counterexample(combo, action, inputs)
+
+ local counterexample = false
+ for key,bool in ipairs(inputs) do --check the input for counter-examples
+ if input.IsKeyDown(key) then
+ if pace.shortcuts_ignored_keys[key] then continue end
+ if not table.HasValue(combo, input.GetKeyName(key)) then --any keypress that is not in the combo invalidates the combo
+ --some keys don't count as counterexamples??
+ --random windows or capslocktoggle keys being pressed screw up the input
+ --bulk select should allow rolling select with the scrolling options
+
+ if key == pace.BulkSelectKey and not action == "editor_up" and not action == "editor_down" and not action == "editor_pageup" and not action == "editor_pagedown" then
+ counterexample = true
+ elseif not pace.passthrough_keys[key] and key ~= pace.BulkSelectKey then
+ counterexample = true
+ end
- if input.IsKeyDown(KEY_LALT) and input.IsKeyDown(KEY_E) then
- pace.Call("ToggleFocus", true)
- last = RealTime() + 0.2
+ if pace.passthrough_keys[key] or key == pace.BulkSelectKey then
+ counterexample = false
+ end
+ end
+
+ end
+ end
+ return counterexample
end
- if input.IsKeyDown(KEY_LCONTROL) and input.IsKeyDown(KEY_E) then
- pace.Call("ToggleFocus")
- last = RealTime() + 0.2
+ if not pace.PACActionShortcut[action] then return false end
+ local final_success = false
+
+ local keynames_str = ""
+ for key,bool in ipairs(provided_inputs) do
+ if bool then keynames_str = keynames_str .. input.GetKeyName(key) .. "," end
end
- if input.IsKeyDown(KEY_LALT) and input.IsKeyDown(KEY_LCONTROL) and input.IsKeyDown(KEY_P) then
- RunConsoleCommand("pac_restart")
+ for i=1,10,1 do --go through each combo slot
+ if pace.PACActionShortcut[action][i] then --is there a combo in that slot
+ combo = pace.PACActionShortcut[action][i]
+ local keynames_str = ""
+
+ local single_match = false
+ if input_contains_one_match(combo, action, provided_inputs) then
+ single_match = true
+ if shortcut_contains_counterexample(combo, action, provided_inputs) then
+ single_match = false
+ end
+ end
+
+ if single_match and do_it then
+ pace.DoShortcutFunc(action)
+ final_success = true
+ --MsgC(Color(50,255,100),"-------------------------\n\n\n\nrun yes " .. action .. "\n" .. keynames_str .. "\n\n\n\n-------------------------")
+ end
+ end
end
- -- Only if the editor is in the foreground
- if pace.IsFocused() then
- if input.IsKeyDown(KEY_LCONTROL) and input.IsKeyDown(KEY_S) then
- pace.Call("ShortcutSave")
- last = RealTime() + 0.2
+ return final_success
+end
+
+function pace.AssignEditorShortcut(action, tbl, index)
+ print("received a new shortcut assignation")
+
+ pace.PACActionShortcut[action] = pace.PACActionShortcut[action] or {}
+ pace.PACActionShortcut[action][index] = pace.PACActionShortcut[action][index] or {}
+
+ if table.IsEmpty(tbl) or not tbl then
+ pace.PACActionShortcut[action][index] = nil
+ print("wiped shortcut " .. action .. " off index " .. index)
+ return
+ end
+ --validate tbl argument
+ for i,key in pairs(tbl) do
+ print(i,key)
+ if not isnumber(i) then print("passed a wrong table") return end
+ if not isstring(key) then print("passed a wrong table") return end
+ end
+ pace.PACActionShortcut[action][index] = tbl
+end
+
+function pace.DoShortcutFunc(action)
+
+ pace.delaybulkselect = RealTime() + 0.5
+ pace.delayshortcuts = RealTime() + 0.2
+ pace.delaymovement = RealTime() + 1
+
+ if action == "editor_up" then pace.DoScrollControls(action)
+ elseif action == "editor_down" then pace.DoScrollControls(action)
+ elseif action == "editor_pageup" then pace.DoScrollControls(action)
+ elseif action == "editor_pagedown" then pace.DoScrollControls(action)
+ end
+ if action == "editor_node_expand" then pace.Call("VariableChanged", pace.current_part, "EditorExpand", true)
+ elseif action == "editor_node_collapse" then pace.Call("VariableChanged", pace.current_part, "EditorExpand", false) end
+
+ if action == "redo" then pace.Redo(pace.current_part) pace.delayshortcuts = RealTime() end
+ if action == "undo" then pace.Undo(pace.current_part) pace.delayshortcuts = RealTime() end
+ if action == "delete" then pace.RemovePart(pace.current_part) end
+ if action == "hide" then pace.current_part:SetHide(not pace.current_part:GetHide()) pace.PopulateProperties(pace.current_part) end
+
+ if action == "copy" then pace.Copy(pace.current_part) end
+ if action == "cut" then pace.Cut(pace.current_part) end
+ if action == "paste" then pace.Paste(pace.current_part) end
+ if action == "clone" then pace.Clone(pace.current_part) end
+ if action == "save" then pace.Call("ShortcutSave") end
+ if action == "load" then
+ local function add_expensive_submenu_load(pnl, callback)
+ local old = pnl.OnCursorEntered
+ pnl.OnCursorEntered = function(...)
+ callback()
+ pnl.OnCursorEntered = old
+ return old(...)
+ end
+ end
+ local menu = DermaMenu()
+ local x,y = input.GetCursorPos()
+ menu:SetPos(x,y)
+
+ menu.GetDeleteSelf = function() return false end
+
+ menu:AddOption(L"load from url", function()
+ Derma_StringRequest(
+ L"load parts",
+ L"Some indirect urls from on pastebin, dropbox, github, etc are handled automatically. Pasting the outfit's file contents into the input field will also work.",
+ "",
+
+ function(name)
+ pace.LoadParts(name, clear, override_part)
+ end
+ )
+ end):SetImage(pace.MiscIcons.url)
+
+ menu:AddOption(L"load from clipboard", function()
+ pace.MultilineStringRequest(
+ L"load parts from clipboard",
+ L"Paste the outfits content here.",
+ "",
+
+ function(name)
+ local data,err = pace.luadata.Decode(name)
+ if data then
+ pace.LoadPartsFromTable(data, clear, override_part)
+ end
+ end
+ )
+ end):SetImage(pace.MiscIcons.paste)
+
+ if not override_part and pace.example_outfits then
+ local examples, pnl = menu:AddSubMenu(L"examples")
+ pnl:SetImage(pace.MiscIcons.help)
+ examples.GetDeleteSelf = function() return false end
+
+ local sorted = {}
+ for k,v in pairs(pace.example_outfits) do sorted[#sorted + 1] = {k = k, v = v} end
+ table.sort(sorted, function(a, b) return a.k < b.k end)
+
+ for _, data in pairs(sorted) do
+ examples:AddOption(data.k, function() pace.LoadPartsFromTable(data.v) end)
+ :SetImage(pace.MiscIcons.outfit)
+ end
end
- -- CTRL + (W)ear?
- if input.IsKeyDown(KEY_LCONTROL) and input.IsKeyDown(KEY_N) then
- pace.Call("ShortcutWear")
- last = RealTime() + 0.2
+ menu:AddSpacer()
+
+ pace.AddOneDirectorySavedPartsToMenu(menu, "templates", "templates")
+ pace.AddOneDirectorySavedPartsToMenu(menu, "__backup_save", "backups")
+
+ menu:AddSpacer()
+ do
+ local menu, icon = menu:AddSubMenu(L"load (expensive)", function() pace.LoadParts(nil, true) end)
+ menu:SetDeleteSelf(false)
+ icon:SetImage(pace.MiscIcons.load)
+ add_expensive_submenu_load(icon, function() pace.AddSavedPartsToMenu(menu, true) end)
end
- if input.IsKeyDown(KEY_LCONTROL) and input.IsKeyDown(KEY_T) then
- pace.SetTPose(not pace.GetTPose())
- last = RealTime() + 0.2
+ menu:SetMaxHeight(ScrH() - y)
+ menu:MakePopup()
+
+ end
+ if action == "wear" then pace.Call("ShortcutWear") end
+
+ if action == "hide_editor" then pace.Call("ToggleFocus") pace.delaymovement = RealTime() pace.delaybulkselect = RealTime() end
+ if action == "hide_editor_visible" then pace.Call("ToggleFocus", true) end
+ if action == "panic" then pac.Panic() end
+ if action == "restart" then RunConsoleCommand("pac_restart") end
+ if action == "collapse_all" then
+
+ local part = pace.current_part
+
+ if not part or not part:IsValid() then
+ pace.FlashNotification('No part to collapse')
+ else
+
+ end
+ part:CallRecursive('SetEditorExpand', GetConVar("pac_reverse_collapse"):GetBool())
+ pace.RefreshTree(true)
+ end
+ if action == "expand_all" then
+
+ local part = pace.current_part
+
+ if not part or not part:IsValid() then
+ pace.FlashNotification('No part to collapse')
+ else
+
end
+ part:CallRecursive('SetEditorExpand', not GetConVar("pac_reverse_collapse"):GetBool())
+ pace.RefreshTree(true)
+ end
- if input.IsKeyDown(KEY_LCONTROL) and input.IsKeyDown(KEY_F) then
+ if action == "partmenu" then pace.OnPartMenu(pace.current_part) end
+ if action == "property_search_current_part" then
+ if pace.properties.search:IsVisible() then
+ pace.properties.search:SetVisible(false)
+ pace.properties.search:SetEnabled(false)
+ pace.property_searching = false
+ else
pace.properties.search:SetVisible(true)
pace.properties.search:RequestFocus()
pace.properties.search:SetEnabled(true)
pace.property_searching = true
+ end
+
+ end
+ if action == "property_search_in_tree" then
+ if pace.tree_search_open then
+ pace.tree_searcher:Remove()
+ else
+ pace.OpenTreeSearch()
+ end
+ end
+ if action == "add_part" then pace.OnAddPartMenu(pace.current_part) end
+ if action == "toolbar_tools" then
+ menu = DermaMenu()
+ local x,y = input.GetCursorPos()
+ menu:SetPos(x,y)
+ pace.AddToolsToMenu(menu)
+ end
+ if action == "toolbar_pac" then
+ menu = DermaMenu()
+ local x,y = input.GetCursorPos()
+ menu:AddOption("pac")
+ menu:SetPos(x,y)
+ pace.PopulateMenuBarTab(menu, "pac")
+ end
+ if action == "toolbar_options" then
+ menu = DermaMenu()
+ local x,y = input.GetCursorPos()
+ menu:SetPos(x,y)
+ pace.PopulateMenuBarTab(menu, "options")
+ end
+ if action == "toolbar_player" then
+ menu = DermaMenu()
+ local x,y = input.GetCursorPos()
+ menu:SetPos(x,y)
+ pace.PopulateMenuBarTab(menu, "player")
+
+ end
+ if action == "toolbar_view" then
+ menu = DermaMenu()
+ local x,y = input.GetCursorPos()
+ menu:SetPos(x,y)
+ pace.PopulateMenuBarTab(menu, "view")
+ end
+
+ if action == "zoom_panel" then
+ pace.PopupMiniFOVSlider()
+ end
+ if action == "reset_zoom" then
+ pace.ResetZoom()
+ end
+ if action == "reset_view_position" then
+ pace.ResetView()
+ end
+ if action == "view_orthographic" then
+ pace.OrthographicView()
+ end
+ if action == "view_follow_entity" then
+ GetConVar("pac_camera_follow_entity"):SetBool(not GetConVar("pac_camera_follow_entity"):GetBool())
+ end
+ if action == "reset_eyeang" then
+ pace.ResetEyeAngles()
+ elseif action == "reset_eyeang_pitch" then
+ pace.ResetEyeAngles(true)
+ end
+ if action == "view_follow_entity_ang_frontback" then
+ pace.ResetEyeAngles(true)
+ local b = GetConVar("pac_camera_follow_entity_ang"):GetBool()
+ GetConVar("pac_camera_follow_entity_ang_use_side"):SetBool(false)
+ if not b then
+ pace.view_reversed = 1
+ GetConVar("pac_camera_follow_entity_ang"):SetBool(true)
+ timer.Simple(0, function() pace.FlashNotification("view_follow_entity_ang_frontback (back)") end)
+ else
+ if pace.view_reversed == -1 then
+ GetConVar("pac_camera_follow_entity_ang"):SetBool(false)
+ timer.Simple(0, function() pace.FlashNotification("view_follow_entity_ang_frontback (disable)") end)
+ else
+ timer.Simple(0, function() pace.FlashNotification("view_follow_entity_ang_frontback (front)") end)
+ end
+ pace.view_reversed = -pace.view_reversed
+ end
+ end
+ if action == "view_follow_entity_sideview" then
+ pace.ResetEyeAngles(true)
+ local b = GetConVar("pac_camera_follow_entity_ang"):GetBool()
+ GetConVar("pac_camera_follow_entity_ang_use_side"):SetBool(true)
+ if not b then
+ pace.view_reversed = 1
+ GetConVar("pac_camera_follow_entity_ang"):SetBool(true)
+ timer.Simple(0, function() pace.FlashNotification("view_follow_entity_sideview (left)") end)
+ else
+ if pace.view_reversed == -1 then
+ GetConVar("pac_camera_follow_entity_ang"):SetBool(false)
+ timer.Simple(0, function() pace.FlashNotification("view_follow_entity_sideview (disable)") end)
+ else
+ timer.Simple(0, function() pace.FlashNotification("view_follow_entity_sideview (right)") end)
+ end
+ pace.view_reversed = -pace.view_reversed
+ end
+ end
+ if action == "T_Pose" or action == "t_pose" then pace.SetTPose(not pace.GetTPose()) end
+
+ if action == "bulk_select" then
+ pace.DoBulkSelect(pace.current_part)
+ end
+ if action == "clear_bulkselect" then pace.ClearBulkList() end
+ if action == "bulk_copy" then pace.BulkCopy(pace.current_part) end --deprecated keyword
+ if action == "copy_bulkselect" then pace.BulkCopy(pace.current_part) end
+ if action == "bulk_insert" then pace.BulkCutPaste(pace.current_part) end
+ if action == "bulk_delete" then pace.BulkRemovePart() end
+ if action == "bulk_pack" then
+ root = pac.CreatePart("group")
+ for i,v in ipairs(pace.BulkSelectList) do
+ v:SetParent(root)
+ end
+ end
+ if action == "bulk_paste_1" then pace.BulkPasteFromBulkSelectToSinglePart(pace.current_part) end
+ if action == "bulk_paste_2" then pace.BulkPasteFromSingleClipboard(pace.current_part) end
+ if action == "bulk_paste_3" then pace.BulkPasteFromBulkClipboard(pace.current_part) end
+ if action == "bulk_paste_4" then pace.BulkPasteFromBulkClipboardToBulkSelect(pace.current_part) end
+
+ if action == "bulk_paste_properties_1" then
+ pace.Copy(pace.current_part)
+ for _,v in ipairs(pace.BulkSelectList) do
+ pace.PasteProperties(v)
+ end
+ end
+ if action == "bulk_paste_properties_2" then
+ for _,v in ipairs(pace.BulkSelectList) do
+ pace.PasteProperties(v)
+ end
+ end
+ if action == "bulk_hide" then pace.BulkHide() pace.PopulateProperties(pace.current_part) end
+
+ if action == "help_info_popup" then
+ if pace.floating_popup_reserved then
+ pace.floating_popup_reserved:Remove()
+ end
+
+ --[[pac.InfoPopup("Looks like you don't have an active part. You should right click and go make one to get started", {
+ obj_type = "screen",
+ clickfunc = function() pace.OnAddPartMenu(pace.current_part) end,
+ hoverfunc = "open",
+ pac_part = false
+ }, ScrW()/2, ScrH()/2)]]
+
+
+ local popup_setup_tbl = {
+ obj_type = "",
+ clickfunc = function() pace.OnAddPartMenu(pace.current_part) end,
+ hoverfunc = "open",
+ pac_part = pace.current_part,
+ panel_exp_width = 900, panel_exp_height = 400
+ }
+
+ --obj_type types
+ local popup_prefered_type = GetConVar("pac_popups_preferred_location"):GetString()
+ popup_setup_tbl.obj_type = popup_prefered_type
+
+ if popup_prefered_type == "pac tree label" then
+ popup_setup_tbl.obj = pace.current_part.pace_tree_node
+ pace.floating_popup_reserved = pace.current_part:SetupEditorPopup(nil, true, popup_setup_tbl)
+
+ elseif popup_prefered_type == "part world" then
+ popup_setup_tbl.obj = pace.current_part
+ pace.floating_popup_reserved = pace.current_part:SetupEditorPopup(nil, true, popup_setup_tbl)
+
+ elseif popup_prefered_type == "screen" then
+ pace.floating_popup_reserved = pace.current_part:SetupEditorPopup(nil, true, popup_setup_tbl, ScrW()/2, ScrH()/2)
+
+ elseif popup_prefered_type == "cursor" then
+ pace.floating_popup_reserved = pace.current_part:SetupEditorPopup(nil, true, popup_setup_tbl, input.GetCursorPos())
+
+ elseif popup_prefered_type == "tracking cursor" then
+ pace.floating_popup_reserved = pace.current_part:SetupEditorPopup(nil, true, popup_setup_tbl, input.GetCursorPos())
+
+ elseif popup_prefered_type == "menu bar" then
+ popup_setup_tbl.obj = pace.Editor
+ pace.floating_popup_reserved = pace.current_part:SetupEditorPopup(nil, true, popup_setup_tbl)
+
+ end
+
+
+
+
+ --[[if IsValid(pace.current_part) then
+ pac.AttachInfoPopupToPart(pace.current_part)
+ else
+ pac.InfoPopup("Looks like you don't have an active part. You should right click and go make one to get started", {
+ obj_type = "screen",
+ --hoverfunc = function() pace.OnAddPartMenu(pace.current_part) end,
+ pac_part = false
+ }, ScrW()/2, ScrH()/2)
+ end]]
+ end
+
+ if action == "ultra_cleanup" then
+ pace.UltraCleanup(pace.current_part)
+ end
+
+ if action == "arraying_menu" then
+ pace.OpenArrayingMenu(pace.current_part)
+ end
+
+ if action == "bulk_morph" then
+ pace.BulkMorphProperty()
+ end
+
+ if action == "criteria_process" then
+ pace.PromptProcessPartsByCriteria(pace.current_part)
+ end
+
+end
+
+pace.delaybulkselect = 0
+pace.delayshortcuts = 0
+pace.delaymovement = 0
+
+--only check once. what does this mean?
+--if a shortcut is SUCCESSFULLY run (check_input = false), stop checking until inputs are reset (if no_input then check_input = true end)
+--always refresh the inputs, but check if we stay the same before checking the shortcuts!
+--
+
+local skip = false
+local no_input_override = false
+local has_run_something = false
+local previous_inputs_str = ""
+
+function pace.CheckShortcuts()
+ if GetConVar("pac_editor_shortcuts_legacy_mode"):GetBool() then
+ if gui.IsConsoleVisible() then return end
+ if not pace.Editor or not pace.Editor:IsValid() then return end
+ if last > RealTime() or input.IsMouseDown(MOUSE_LEFT) then return end
+
+ if input.IsKeyDown(KEY_LALT) and input.IsKeyDown(KEY_E) then
+ pace.Call("ToggleFocus", true)
+ last = RealTime() + 0.2
+ end
+
+ if input.IsKeyDown(KEY_LCONTROL) and input.IsKeyDown(KEY_E) then
+ pace.Call("ToggleFocus")
last = RealTime() + 0.2
end
+ if input.IsKeyDown(KEY_LALT) and input.IsKeyDown(KEY_LCONTROL) and input.IsKeyDown(KEY_P) then
+ RunConsoleCommand("pac_restart")
+ end
+
+ -- Only if the editor is in the foreground
+ if pace.IsFocused() then
+ if input.IsKeyDown(KEY_LCONTROL) and input.IsKeyDown(KEY_S) then
+ pace.Call("ShortcutSave")
+ last = RealTime() + 0.2
+ end
+
+ -- CTRL + (W)ear?
+ if input.IsKeyDown(KEY_LCONTROL) and input.IsKeyDown(KEY_N) then
+ pace.Call("ShortcutWear")
+ last = RealTime() + 0.2
+ end
+
+ if input.IsKeyDown(KEY_LCONTROL) and input.IsKeyDown(KEY_T) then
+ pace.SetTPose(not pace.GetTPose())
+ last = RealTime() + 0.2
+ end
+
+ if input.IsKeyDown(KEY_LCONTROL) and input.IsKeyDown(KEY_F) then
+ if not input.IsKeyDown(KEY_LSHIFT)then
+ pace.properties.search:SetVisible(true)
+ pace.properties.search:RequestFocus()
+ pace.properties.search:SetEnabled(true)
+ pace.property_searching = true
+
+ last = RealTime() + 0.2
+ else
+ pace.OpenTreeSearch()
+ end
+ end
+
+ if input.IsKeyDown(KEY_F1) then
+ last = RealTime() + 0.5
+ local new_popup = true
+ if IsValid(pace.legacy_floating_popup_reserved) then
+ new_popup = false
+ if pace.current_part ~= pace.legacy_floating_popup_reserved_part then
+ if IsValid(pace.legacy_floating_popup_reserved) then
+ pace.legacy_floating_popup_reserved:Remove()
+ pace.legacy_floating_popup_reserved = nil
+ pace.legacy_floating_popup_reserved_part = nil
+ end
+ new_popup = true
+ end
+ else
+ pace.legacy_floating_popup_reserved = nil
+ pace.legacy_floating_popup_reserved_part = nil
+ end
+
+ local popup_setup_tbl = {
+ obj_type = "",
+ clickfunc = function() pace.OnAddPartMenu(pace.current_part) end,
+ hoverfunc = "open",
+ pac_part = pace.current_part,
+ panel_exp_width = 900, panel_exp_height = 400,
+ from_legacy = true
+ }
+
+ popup_setup_tbl.obj_type = "pac tree label"
+ popup_setup_tbl.obj = pace.current_part.pace_tree_node
+
+ if new_popup then
+ local created_panel = pace.current_part:SetupEditorPopup(nil, true, popup_setup_tbl)
+ pace.legacy_floating_popup_reserved = created_panel
+ pace.legacy_floating_popup_reserved_part = pace.current_part
+ end
+ pac.AddHook("Think", "killpopupwheneditorunfocused", function()
+ if not pace:IsFocused() then
+ if IsValid(pace.legacy_floating_popup_reserved) then pace.legacy_floating_popup_reserved:Remove() end
+ end
+ if not IsValid(pace.legacy_floating_popup_reserved) then pace.legacy_floating_popup_reserved = nil end
+ end)
+ end
+ end
+ return
+ end
+
+ local input_active = {}
+ local no_input = true
+ no_input_override = false
+ local inputs_str = ""
+ pace.shortcut_inputs_count = 0
+ for i=1,172,1 do --build bool list of all current keys
+ if input.IsKeyDown(i) then
+ if pace.shortcuts_ignored_keys[i] then continue end
+ if pace.passthrough_keys[i] or i == pace.BulkSelectKey then no_input_override = true end
+ input_active[i] = true
+ pace.shortcut_inputs_count = pace.shortcut_inputs_count + 1
+ no_input = false
+ inputs_str = inputs_str .. input.GetKeyName(i) .. " "
+ else input_active[i] = false end
+ end
+
+ if previous_inputs_str ~= inputs_str then
+ if last + 0.2 > RealTime() and has_run_something then
+ skip = true
+ else
+ has_run_something = false
+ end
+
end
+ if no_input then
+ skip = false
+ end
+ previous_inputs_str = inputs_str
+
+
+ if IsValid(vgui.GetKeyboardFocus()) and vgui.GetKeyboardFocus():GetClassName():find('Text') then return end
+ if gui.IsConsoleVisible() then return end
+ if not pace.Editor or not pace.Editor:IsValid() then return end
+
+
+ if skip and not no_input_override then return end
+
+ local starttime = SysTime()
+
+ for action,list_of_lists in pairs(pace.PACActionShortcut) do
+ if not has_run_something then
+ if (action == "hide_editor" or action == "hide_editor_visible") and pace.LookupShortcutsForAction(action, input_active, true) then --we can focus back if editor is not focused
+ --pace.DoShortcutFunc(action)
+ last = RealTime()
+ has_run_something = true
+ check_input = false
+ elseif pace.Focused and pace.LookupShortcutsForAction(action, input_active, true) then --we can't do anything else if not focused
+ --pace.DoShortcutFunc(action)
+ pace.FlashNotification(action)
+ last = RealTime()
+ has_run_something = true
+ check_input = false
+ end
+ end
+ end
+
end
pac.AddHook("Think", "pace_shortcuts", pace.CheckShortcuts)
@@ -254,16 +1156,21 @@ do
end
pac.AddHook("Think", "pace_keyboard_shortcuts", function()
+
if not pace.IsActive() then return end
if not pace.Focused then return end
if IsValid(vgui.GetKeyboardFocus()) and vgui.GetKeyboardFocus():GetClassName():find('Text') then return end
if gui.IsConsoleVisible() then return end
- thinkUndo()
- thinkCopy()
- thinkPaste()
- thinkCut()
- thinkDelete()
- thinkExpandAll()
- thinkCollapseAll()
+ if GetConVar("pac_editor_shortcuts_legacy_mode"):GetBool() then
+ thinkUndo()
+ thinkCopy()
+ thinkPaste()
+ thinkCut()
+ thinkDelete()
+ thinkExpandAll()
+ thinkCollapseAll()
+ end
+
end)
-end
\ No newline at end of file
+end
+
diff --git a/lua/pac3/editor/client/show_outfit_on_use.lua b/lua/pac3/editor/client/show_outfit_on_use.lua
index 9346e52eb..56eee8f57 100644
--- a/lua/pac3/editor/client/show_outfit_on_use.lua
+++ b/lua/pac3/editor/client/show_outfit_on_use.lua
@@ -17,7 +17,7 @@ end
local pac_IsPacOnUseOnly = pac.IsPacOnUseOnly
-hook.Add("PlayerBindPress", "pac_onuse_only", function(ply, bind, isPressed)
+pac.AddHook("PlayerBindPress", "onuse_only", function(ply, bind, isPressed)
if bind ~= "use" and bind ~= "+use" then return end
if bind ~= "+use" and isPressed then return end
if not pac_IsPacOnUseOnly() then return end
@@ -47,7 +47,7 @@ do
weight = 600,
})
- hook.Add("HUDPaint", "pac_onuse_only", function()
+ pac.AddHook("HUDPaint", "onuse_only", function()
if not pac_IsPacOnUseOnly() then return end
local ply = pac.LocalPlayer
local eyes, aim = ply:EyePos(), ply:GetAimVector()
@@ -72,7 +72,7 @@ do
end
function pace.OnUseOnlyUpdates(cvar, ...)
- hook.Call('pace_OnUseOnlyUpdates', nil, ...)
+ pace.Call("OnUseOnlyUpdates", ...)
end
cvars.AddChangeCallback("pac_onuse_only", pace.OnUseOnlyUpdates, "PAC3")
@@ -122,7 +122,7 @@ function pace.HandleOnUseReceivedData(data)
-- behaviour of this (if one of entities on this hook becomes invalid)
-- is undefined if DLib is not installed, but anyway
- hook.Add('pace_OnUseOnlyUpdates', data.owner, function()
+ pac.AddHook('pace_OnUseOnlyUpdates', data.owner, function()
if pac_IsPacOnUseOnly() then
pac.ToggleIgnoreEntity(data.owner, data.owner.pac_onuse_only_check, 'pac_onuse_only')
else
diff --git a/lua/pac3/editor/client/spawnmenu.lua b/lua/pac3/editor/client/spawnmenu.lua
index d18234abe..838809eba 100644
--- a/lua/pac3/editor/client/spawnmenu.lua
+++ b/lua/pac3/editor/client/spawnmenu.lua
@@ -101,18 +101,162 @@ function pace.ClientOptionsMenu(self)
self:Button(L"request outfits", "pac_request_outfits")
end
+CreateClientConVar("pac_limit_sounds_draw_distance", 20000, true, false, "Overall multiplier for PAC3 sounds")
+cvars.AddChangeCallback("pac_limit_sounds_draw_distance", function(_,_,val)
+ if not isnumber(val) then val = 0 end
+ pac.sounds_draw_dist_sqr = val * val
+end)
+pac.sounds_draw_dist_sqr = math.pow(GetConVar("pac_limit_sounds_draw_distance"):GetInt(), 2)
+
+CreateClientConVar("pac_volume", 1, true, false, "Overall multiplier for PAC3 sounds",0,1)
+cvars.AddChangeCallback("pac_volume", function(_,_,val)
+ pac.volume = math.pow(math.Clamp(val,0,1),2) --adjust for the nonlinearity of volume
+ pac.ForceUpdateSoundVolumes()
+end)
+
+pac.volume = math.pow(math.Clamp(GetConVar("pac_volume"):GetFloat(),0,1), 2)
+
+concommand.Add("pac_stopsound", function()
+ pac.StopSound()
+end)
+
function pace.ClientSettingsMenu(self)
if not IsValid(self) then return end
self:Help(L"Performance"):SetFont("DermaDefaultBold")
self:CheckBox(L"Enable PAC", "pac_enable")
self:NumSlider(L"Draw distance:", "pac_draw_distance", 0, 20000, 0)
self:NumSlider(L"Max render time: ", "pac_max_render_time", 0, 100, 0)
+
+ self:Help(L"Sounds"):SetFont("DermaDefaultBold")
+ self:NumSlider(L"Sounds volume", "pac_volume", 0, 1, 2)
+ self:Button(L"Stop sounds", "pac_stopsound")
+
+ self:Help(L"Part limiters"):SetFont("DermaDefaultBold")
+ self:NumSlider(L"Sounds draw distance:", "pac_limit_sounds_draw_distance", 0, 20000, 0)
+ self:NumSlider(L"2D text draw distance:", "pac_limit_text_2d_draw_distance", 0, 20000, 0)
+ self:NumSlider(L"Sunbeams draw distance: ", "pac_limit_sunbeams_draw_distance", 0, 20000, 0)
+ self:NumSlider(L"Shake draw distance: ", "pac_limit_shake_draw_distance", 0, 20000, 0)
+ self:NumSlider(L"Shake max duration: ", "pac_limit_shake_duration", 0, 120, 0)
+ self:NumSlider(L"Shake max amplitude: ", "pac_limit_shake_amplitude", 0, 1000, 0)
+ self:NumSlider(L"Particles max per emission: ", "pac_limit_particles_per_emission", 0, 5000, 0)
+ self:NumSlider(L"Particles max per emitter: ", "pac_limit_particles_per_emitter", 0, 10000, 0)
+end
+
+function pace.AdminSettingsMenu(self)
+ if not LocalPlayer():IsAdmin() then return end
+ if not IsValid(self) then return end
+ self:Button("Open PAC3 settings menu (Admin)", "pace_settings")
+ if GetConVar("pac_sv_block_combat_features_on_next_restart"):GetInt() ~= 0 then
+ self:Help(L"Remember that you have to reinitialize combat parts if you want to enable those that were blocked."):SetFont("DermaDefaultBold")
+ self:Button("Reinitialize combat parts", "pac_sv_reinitialize_missing_combat_parts_remotely")
+ end
+
+ self:Help(L"PAC3 outfits: general server policy"):SetFont("DermaDefaultBold")
+ self:NumSlider(L"Server Draw distance:", "pac_sv_draw_distance", 0, 20000, 0)
+ self:CheckBox(L"Prevent spam with pac_submit", "pac_submit_spam")
+ self:CheckBox(L"Players need to +USE on others to reveal outfits", "pac_onuse_only_force")
+ self:CheckBox(L"Restrict editor camera", "pac_restrictions")
+ self:CheckBox(L"Allow MDL zips", "pac_allow_mdl")
+ self:CheckBox(L"Allow MDL zips for entity", "pac_allow_mdl_entity")
+ self:CheckBox(L"Allow entity model modifier", "pac_modifier_model")
+ self:CheckBox(L"Allow entity size modifier", "pac_modifier_size")
+ self:CheckBox(L"Allow blood color modifier", "pac_allow_blood_color")
+ self:NumSlider(L"Allow prop / other player outfits", "pac_sv_prop_outfits", 0, 2, 0)
+ self:Help(""):SetFont("DermaDefaultBold")--spacers
+ self:Help(""):SetFont("DermaDefaultBold")
+
+ self:Help(L"PAC3 combat: general server policy"):SetFont("DermaDefaultBold")
+ self:NumSlider(L"Rate limiter", "pac_sv_combat_enforce_netrate", 0, 1000, 0)
+ self:NumSlider(L"Distance limiter", "pac_sv_combat_distance_enforced", 0, 64000, 0)
+ self:NumSlider(L"Allowance, in number of messages", "pac_sv_combat_enforce_netrate_buffersize", 0, 400, 0)
+ self:CheckBox(L"Use general prop protection based on player consents", "pac_sv_prop_protection")
+ self:NumSlider(L"Entity limit per combat action", "pac_sv_entity_limit_per_combat_operation", 0, 1000, 0)
+ self:NumSlider(L"Entity limit per player", "pac_sv_entity_limit_per_player_per_combat_operation", 0, 500, 0)
+ self:CheckBox(L"Only specifically allowed users can do pac3 combat actions", "pac_sv_combat_whitelisting")
+ self:Help(""):SetFont("DermaDefaultBold")--spacers
+ self:Help(""):SetFont("DermaDefaultBold")
+
+ self:Help(L"Combat parts (more detailed settings in the full editor settings menu)"):SetFont("DermaDefaultBold")
+ self:Help(L"Damage Zones"):SetFont("DermaDefaultBold")
+ self:CheckBox(L"Enable damage zones", "pac_sv_damage_zone")
+ self:NumSlider(L"Max damage", "pac_sv_damage_zone_max_damage", 0, 268435455, 0)
+ self:NumSlider(L"Max radius", "pac_sv_damage_zone_max_radius", 0, 32767, 0)
+ self:NumSlider(L"Max length", "pac_sv_damage_zone_max_length", 0, 32767, 0)
+ self:CheckBox(L"Enable damage zone dissolve", "pac_sv_damage_zone_allow_dissolve")
+ self:CheckBox(L"Enable ragdoll hitparts", "pac_sv_damage_zone_allow_ragdoll_hitparts")
+
+ self:Help(L"Hitscan"):SetFont("DermaDefaultBold")
+ self:CheckBox(L"Enable hitscan part", "pac_sv_hitscan")
+ self:NumSlider(L"Max damage", "pac_sv_hitscan_max_damage", 0, 268435455, 0)
+ self:CheckBox(L"Force damage division among multi-shot bullets", "pac_sv_hitscan_divide_max_damage_by_max_bullets")
+
+ self:Help(L"Lock part"):SetFont("DermaDefaultBold")
+ self:CheckBox(L"Enable lock part", "pac_sv_lock")
+ self:CheckBox(L"Allow grab", "pac_sv_lock_grab")
+ self:CheckBox(L"Allow teleport", "pac_sv_lock_teleport")
+ self:CheckBox(L"Allow aiming", "pac_sv_lock_aim")
+
+ self:Help(L"Force part"):SetFont("DermaDefaultBold")
+ self:CheckBox(L"Enable force part", "pac_sv_force")
+ self:NumSlider(L"Max amount", "pac_sv_force_max_amount", 0, 10000000, 0)
+ self:NumSlider(L"Max radius", "pac_sv_force_max_radius", 0, 32767, 0)
+ self:NumSlider(L"Max length", "pac_sv_force_max_length", 0, 32767, 0)
+
+ self:Help(L"Health Modifier"):SetFont("DermaDefaultBold")
+ self:CheckBox(L"Enable health modifier", "pac_sv_health_modifier")
+ self:CheckBox(L"Allow changing max health or armor", "pac_sv_health_modifier_allow_maxhp")
+ self:NumSlider(L"Maximum modified health or armor", "pac_sv_health_modifier_max_hp_armor", 0, 100000000, 0)
+ self:NumSlider(L"Minimum combined damage scaling", "pac_sv_health_modifier_min_damagescaling", -10, 1, 2)
+ self:CheckBox(L"Allow extra health bars", "pac_sv_health_modifier_extra_bars")
+ self:CheckBox(L"Allow counted hits mode", "pac_sv_health_modifier_allow_counted_hits")
+ self:NumSlider(L"Maximum combined extra health value", "pac_sv_health_modifier_max_extra_bars_value", 0, 100000000, 0)
+
+ self:Help(L"Projectile part"):SetFont("DermaDefaultBold")
+ self:CheckBox(L"Enable physical projectiles", "pac_sv_projectiles")
+ self:CheckBox(L"Enable custom collide meshes for physical projectiles", "pac_sv_projectile_allow_custom_collision_mesh")
+ self:NumSlider(L"Max speed", "pac_sv_force_max_amount", 0, 5000, 0)
+ self:NumSlider(L"Max physical radius", "pac_sv_projectile_max_phys_radius", 0, 4095, 0)
+ self:NumSlider(L"Max damage radius", "pac_sv_projectile_max_damage_radius", 0, 4095, 0)
+
+ self:Help(L"Player movement part"):SetFont("DermaDefaultBold")
+ self:CheckBox(L"Allow playermovement", "pac_free_movement")
+ self:CheckBox(L"Allow playermovement mass", "pac_player_movement_allow_mass")
+ self:CheckBox(L"Allow physics damage scaling by mass", "pac_player_movement_physics_damage_scaling")
+
end
-local icon = "icon64/pac3.png"
+
+local icon_cvar = CreateConVar("pac_icon", "0", {FCVAR_ARCHIVE}, "Use the new PAC4.5 icon or the old PAC icon.\n0 = use the old one\n1 = use the new one")
+local icon = icon_cvar:GetBool() and "icon64/new pac icon.png" or "icon64/pac3.png"
+
icon = file.Exists("materials/"..icon,'GAME') and icon or "icon64/playermodel.png"
+local function ResetPACIcon()
+ if icon_cvar:GetBool() then icon = "icon64/new pac icon.png" else icon = "icon64/pac3.png" end
+ list.Set(
+ "DesktopWindows",
+ "PACEditor",
+ {
+ title = "PAC Editor",
+ icon = icon,
+ width = 960,
+ height = 700,
+ onewindow = true,
+ init = function(icn, pnl)
+ pnl:Remove()
+ RunConsoleCommand("pac_editor")
+ end
+ }
+ )
+ RunConsoleCommand("spawnmenu_reload")
+end
+
+cvars.AddChangeCallback("pac_icon", ResetPACIcon)
+
+concommand.Add("pac_change_icon", function() RunConsoleCommand("pac_icon", (not icon_cvar:GetBool()) and "1" or "0") ResetPACIcon() end)
+
+
list.Set(
"DesktopWindows",
"PACEditor",
@@ -153,6 +297,17 @@ hook.Add("PopulateToolMenu", "pac_spawnmenu", function()
{
}
)
+ spawnmenu.AddToolMenuOption(
+ "Utilities",
+ "PAC",
+ "PAC3Admin",
+ L"Admin",
+ "",
+ "",
+ pace.AdminSettingsMenu,
+ {
+ }
+ )
end)
if IsValid(g_ContextMenu) and CreateContextMenu then
diff --git a/lua/pac3/editor/client/tools.lua b/lua/pac3/editor/client/tools.lua
index 8a1ebad8c..2ddc2f2a1 100644
--- a/lua/pac3/editor/client/tools.lua
+++ b/lua/pac3/editor/client/tools.lua
@@ -1,5 +1,6 @@
local L = pace.LanguageString
pace.Tools = {}
+include("parts.lua")
function pace.AddToolsToMenu(menu)
menu.GetDeleteSelf = function() return false end
@@ -372,7 +373,7 @@ pace.AddTool(L"import editor tool from file...", function()
Derma_StringRequest(L"filename", L"relative to garrysmod/data/pac3_editor/tools/", "mytool.txt", function(toolfile)
if file.Exists("pac3_editor/tools/" .. toolfile,"DATA") then
local toolstr = file.Read("pac3_editor/tools/" .. toolfile,"DATA")
- local ctoolstr = [[pace.AddTool(L"]] .. toolfile .. [[", function(part, suboption) ]] .. toolstr .. " end)"
+ local ctoolstr = [[pace.AddTool("]] .. toolfile .. [[", function(part, suboption) ]] .. toolstr .. " end)"
RunStringEx(ctoolstr, "pac_editor_import_tool")
pac.LocalPlayer:ConCommand("pac_editor") --close and reopen editor
else
@@ -390,7 +391,7 @@ pace.AddTool(L"import editor tool from url...", function()
local function ToolDLSuccess(body)
local toolname = pac.PrettifyName(toolurl:match(".+/(.-)%."))
local toolstr = body
- local ctoolstr = [[pace.AddTool(L"]] .. toolname .. [[", function(part, suboption)]] .. toolstr .. " end)"
+ local ctoolstr = [[pace.AddTool("]] .. toolname .. [[", function(part, suboption)]] .. toolstr .. " end)"
RunStringEx(ctoolstr, "pac_editor_import_tool")
pac.LocalPlayer:ConCommand("pac_editor") --close and reopen editor
end
@@ -784,6 +785,62 @@ pace.AddTool(L"dump player submaterials", function()
end
end)
+pace.AddTool(L"dump model submaterials", function(part)
+ if part.ClassName == "model" or part.ClassName == "model2" then
+ for id,mat in pairs(part:GetOwner():GetMaterials()) do
+ chat.AddText(("%d %s"):format(id,tostring(mat)))
+ end
+ end
+end)
+
+pace.AddTool(L"proxy/event: Engrave targets", function(part)
+ local function reassign(part)
+ if part.ClassName == "proxy" then
+ if not IsValid(part.TargetPart) then
+ if part.AffectChildren and table.Count(part:GetChildren()) == 1 then
+ part:SetTargetPart(part:GetChildren()[1])
+ part:SetAffectChildren(nil)
+ else
+ part:SetTargetPart(part:GetParent())
+ end
+ end
+ elseif part.ClassName == "event" then
+ if not IsValid(part.DestinationPart) then
+ if part.AffectChildrenOnly == true and table.Count(part:GetChildren()) == 1 then
+ part:SetDestinationPart(part:GetChildren()[1])
+ elseif part.AffectChildrenOnly == false then
+ part:SetDestinationPart(part:GetParent())
+ end
+ end
+ end
+ end
+ if part ~= part:GetRootPart() then
+ reassign(part)
+ else
+ for i,part2 in pairs(pac.GetLocalParts()) do
+ reassign(part2)
+ end
+ end
+end)
+
+pace.AddTool(L"Process by Criteria", function(part)
+ pace.PromptProcessPartsByCriteria(part)
+end)
+
+--aka pace.UltraCleanup
+pace.AddTool(L"Destroy hidden parts, proxies and events", function(part)
+
+ if not part then part = pace.current_part end
+ root = part:GetRootPart()
+
+ pnl = Derma_Query("Only do this if you know what you're doing!\nMark parts as important in their notes to protect them.", "Warning",
+ "Destroy!", function() pace.UltraCleanup( root ) end,
+ "cancel", nil
+ )
+ pnl:SetWidth(300)
+
+end)
+
pace.AddTool(L"stop all custom animations", function()
pac.animations.StopAllEntityAnimations(pac.LocalPlayer)
pac.animations.ResetEntityBoneMatrix(pac.LocalPlayer)
diff --git a/lua/pac3/editor/client/util.lua b/lua/pac3/editor/client/util.lua
index 598f349cb..4df3d63d2 100644
--- a/lua/pac3/editor/client/util.lua
+++ b/lua/pac3/editor/client/util.lua
@@ -145,12 +145,26 @@ function pace.MessagePrompt( strText, strTitle, strButtonText )
local DScrollPanel = vgui.Create( "DScrollPanel", Window )
DScrollPanel:Dock( FILL )
- local Text = DScrollPanel:Add("DLabel")
- Text:SetText( strText or "Message Text" )
- Text:SetTextColor( color_white )
- Text:Dock(FILL)
- Text:SetAutoStretchVertical(true)
- Text:SetWrap(true)
+ if not pace.alternate_message_prompts and (strText and (#strText < 800)) then
+ local Text = DScrollPanel:Add("DLabel")
+ Text:SetText( strText or "Message Text" )
+ Text:SetTextColor( color_white )
+ Text:Dock(FILL)
+ Text:SetAutoStretchVertical(true)
+ Text:SetWrap(true)
+ else --hack for more text length / alternative style using RichText
+ local Text = DScrollPanel:Add("RichText")
+ Text:SetText("")
+ Text:AppendText(strText or "Message Text")
+ Text:SetBGColor(0,0,0,0)
+ Text:Dock(FILL)
+ Text:SetTall(240)
+ Text:SetFGColor(255,255,255,255)
+ function Text:PerformLayout()
+ Text:SetBGColor(0,0,0,0)
+ Text:SetFGColor(255,255,255,255)
+ end
+ end
local Button = vgui.Create( "DButton", Window )
Button:SetText( strButtonText or "OK" )
@@ -194,7 +208,7 @@ function pace.MultilineStringRequest( strTitle, strText, strDefaultText, fnEnter
TextEntry:SetMultiline(true)
TextEntry:Dock(FILL)
TextEntry:SetUpdateOnType(true)
- TextEntry.OnChange = function(self) self:SetText(self:GetValue():gsub("\t", " ")) end
+ TextEntry.OnChange = function(self) local caret = self:GetCaretPos() self:SetText(self:GetValue():gsub("\t", " ")) self:SetCaretPos(caret) end
TextEntry.OnEnter = function() Window:Close() fnEnter( TextEntry:GetValue() ) end
local ButtonPanel = vgui.Create( "DPanel", Window )
@@ -227,4 +241,4 @@ function pace.MultilineStringRequest( strTitle, strText, strDefaultText, fnEnter
return Window
-end
\ No newline at end of file
+end
diff --git a/lua/pac3/editor/client/view.lua b/lua/pac3/editor/client/view.lua
index 8f04a0905..7b103ccd4 100644
--- a/lua/pac3/editor/client/view.lua
+++ b/lua/pac3/editor/client/view.lua
@@ -12,6 +12,144 @@ acsfnc("Pos", Vector(5,5,5))
acsfnc("Angles", Angle(0,0,0))
acsfnc("FOV", 75)
+function pace.GoTo(obj, mode, extra, alt_move)
+ if not obj then return end
+ if mode == "view" and (obj.GetWorldPosition or isentity(obj)) then
+
+ extra = extra or {radius = 75} --if no 3rd arg, assume a basic 75 distance
+ extra.radius = extra.radius or 75 --if the table is wrong, force insert a default 75 distance
+ if alt_move ~= nil then --repeated hits = reverse? or come back?
+ if alt_move == true then
+ extra.radius = -extra.radius
+ elseif alt_move == false then
+
+ end
+ end
+ local obj_pos
+ local angfunc
+ if obj.GetWorldPosition then
+ obj_pos = obj:GetWorldPosition()
+ elseif isentity(obj) then
+ obj_pos = obj:GetPos() + obj:OBBCenter()
+ end
+ pace.ViewAngles = (pace.ViewPos - obj_pos):Angle()
+ if extra.axis then
+ local vec
+ local sgn = extra.radius > 0 and 1 or -1
+ if obj.GetWorldPosition then
+ if extra.axis == "x" then
+ vec = obj:GetWorldAngles():Forward()
+ elseif extra.axis == "y" then
+ vec = obj:GetWorldAngles():Right()
+ elseif extra.axis == "z" then
+ vec = obj:GetWorldAngles():Up()
+ elseif extra.axis == "world_x" then
+ vec = Vector(1,0,0)
+ elseif extra.axis == "world_y" then
+ vec = Vector(0,1,0)
+ elseif extra.axis == "world_z" then
+ vec = Vector(0,0,1)
+ end
+ elseif isentity(obj) then
+ local ang = obj:GetAngles()
+ ang.p = 0
+ if extra.axis == "x" then
+ vec = ang:Forward()
+ elseif extra.axis == "y" then
+ vec = ang:Right()
+ elseif extra.axis == "z" then
+ vec = ang:Up()
+ elseif extra.axis == "world_x" then
+ vec = Vector(1,0,0)
+ elseif extra.axis == "world_y" then
+ vec = Vector(0,1,0)
+ elseif extra.axis == "world_z" then
+ vec = Vector(0,0,1)
+ end
+ end
+ vec = sgn * vec
+ local viewpos = obj_pos - vec * math.abs(extra.radius)
+ pace.ViewPos = viewpos
+ pace.ViewAngles = (obj_pos - viewpos):Angle()
+ pace.ViewAngles:Normalize()
+ return
+ end
+ pace.ViewAngles:Normalize()
+ pace.ViewPos = obj_pos + (obj_pos - pace.ViewPos):GetNormalized() * (extra.radius or 75)
+ elseif mode == "treenode" and (obj.pace_tree_node or ispanel(obj)) then
+ local part
+ if obj.pace_tree_node then part = obj elseif ispanel(obj) then part = obj.part end
+ local parent = part:GetParent()
+ while IsValid(parent) and (parent:GetParent() ~= parent) do
+ parent.pace_tree_node:SetExpanded(true)
+ parent = parent:GetParent()
+ if parent:IsValid() then
+ parent.pace_tree_node:SetExpanded(true)
+ end
+ end
+ if part.pace_tree_node then
+ pace.tree:ScrollToChild(part.pace_tree_node)
+ end
+ elseif mode == "property" then
+ pace.FlashProperty(obj, extra or "Name")
+ end
+end
+
+pace.camera_forward_bind = CreateClientConVar("pac_editor_camera_forward_bind", "w", true)
+pace.camera_back_bind = CreateClientConVar("pac_editor_camera_back_bind", "s", true)
+pace.camera_moveleft_bind = CreateClientConVar("pac_editor_camera_moveleft_bind", "a", true)
+pace.camera_moveright_bind = CreateClientConVar("pac_editor_camera_moveright_bind", "d", true)
+pace.camera_up_bind = CreateClientConVar("pac_editor_camera_up_bind", "space", true)
+pace.camera_down_bind = CreateClientConVar("pac_editor_camera_down_bind", "", true)
+pace.camera_slow_bind = CreateClientConVar("pac_editor_camera_slow_bind", "ctrl", true)
+pace.camera_speed_bind = CreateClientConVar("pac_editor_camera_speed_bind", "shift", true)
+
+pace.max_fov = 100
+pace.camera_roll_drag_bind = CreateClientConVar("pac_editor_camera_roll_bind", "", true)
+pace.roll_snapping = CreateClientConVar("pac_camera_roll_snap", "0", true)
+
+pace.camera_orthographic_cvar = CreateClientConVar("pac_camera_orthographic", "0", true)
+pace.camera_orthographic = pace.camera_orthographic_cvar:GetBool()
+pace.viewlock_mode = ""
+
+function pace.OrthographicView(b)
+ if b == nil then b = not pace.camera_orthographic end
+ pace.camera_orthographic = b
+ pace.camera_orthographic_cvar:SetBool(tobool(b or false))
+ if pace.Editor and pace.Editor.zoomslider then
+ if pace.camera_orthographic then
+ timer.Simple(1, function() pace.FlashNotification("Switched to orthographic mode") end)
+ pace.Editor.zoomslider:SetText("Ortho. Width")
+ pace.Editor.zoomslider:SetValue(50)
+ pace.Editor.ortho_nearz:Show()
+ pace.Editor.ortho_farz:Show()
+ else
+ timer.Simple(1, function() pace.FlashNotification("Switched to normal FOV mode") end)
+ pace.Editor.zoomslider:SetText("Camera FOV")
+ pace.Editor.zoomslider:SetValue(75)
+ pace.Editor.ortho_nearz:Hide()
+ pace.Editor.ortho_farz:Hide()
+ end
+ pace.RefreshZoomBounds(pace.Editor.zoomslider)
+ end
+end
+
+cvars.AddChangeCallback("pac_camera_orthographic", function(name, old, new)
+ pace.OrthographicView(tobool(new))
+end, "pac_update_ortho")
+
+pace.camera_movement_binds = {
+ ["forward"] = pace.camera_forward_bind,
+ ["back"] = pace.camera_back_bind,
+ ["moveleft"] = pace.camera_moveleft_bind,
+ ["moveright"] = pace.camera_moveright_bind,
+ ["up"] = pace.camera_up_bind,
+ ["down"] = pace.camera_down_bind,
+ ["slow"] = pace.camera_slow_bind,
+ ["speed"] = pace.camera_speed_bind,
+ ["roll_drag"] = pace.camera_roll_drag_bind
+}
+
function pace.GetViewEntity()
return pace.ViewEntity:IsValid() and pace.ViewEntity or pac.LocalPlayer
end
@@ -47,16 +185,24 @@ end
function pace.SetZoom(fov, smooth)
if smooth then
- pace.ViewFOV = Lerp(FrameTime()*10, pace.ViewFOV, math.Clamp(fov,1,100))
+ if pace.camera_orthographic then
+ pace.ViewFOV = Lerp(FrameTime()*10, pace.ViewFOV, math.Clamp(fov,-10000,10000))
+ return
+ end
+ pace.ViewFOV = Lerp(FrameTime()*10, pace.ViewFOV, math.Clamp(fov,1,pace.max_fov))
else
- pace.ViewFOV = math.Clamp(fov,1,100)
+ if pace.camera_orthographic then
+ pace.ViewFOV = math.Clamp(fov,-10000,10000)
+ return
+ end
+ pace.ViewFOV = math.Clamp(fov,1,pace.max_fov)
end
end
function pace.ResetZoom()
pace.zoom_reset = 75
end
-
+
local worldPanel = vgui.GetWorldPanel();
function worldPanel.OnMouseWheeled( self, scrollDelta )
if IsValid(pace.Editor) then
@@ -89,6 +235,7 @@ function pace.GUIMousePressed(mc)
if mc == MOUSE_LEFT and not pace.editing_viewmodel then
held_ang = pace.ViewAngles * 1
+ held_ang:Normalize()
held_mpos = Vector(input.GetCursorPos())
end
@@ -119,17 +266,6 @@ function pace.GUIMouseReleased(mc)
if pace.editing_viewmodel or pace.editing_hands then return end
mcode = nil
- if not GetConVar("pac_enable_editor_view"):GetBool() then pace.EnableView(true)
- else
- pac.RemoveHook("CalcView", "camera_part")
- pac.AddHook("GUIMousePressed", "editor", pace.GUIMousePressed)
- pac.AddHook("GUIMouseReleased", "editor", pace.GUIMouseReleased)
- pac.AddHook("ShouldDrawLocalPlayer", "editor", pace.ShouldDrawLocalPlayer, DLib and -4 or ULib and -1 or nil)
- pac.AddHook("CalcView", "editor", pace.CalcView, DLib and -4 or ULib and -1 or nil)
- pac.AddHook("HUDPaint", "editor", pace.HUDPaint)
- pac.AddHook("HUDShouldDraw", "editor", pace.HUDShouldDraw)
- pac.AddHook("PostRenderVGUI", "editor", pace.PostRenderVGUI)
- end
end
local function set_mouse_pos(x, y)
@@ -141,11 +277,34 @@ end
local WORLD_ORIGIN = Vector(0, 0, 0)
+local function MovementBindDown(name)
+ return input.IsButtonDown(input.GetKeyCode(pace.camera_movement_binds[name]:GetString()))
+end
+
+local follow_entity_ang = CreateClientConVar("pac_camera_follow_entity_ang", "0", true)
+local follow_entity_ang_side = CreateClientConVar("pac_camera_follow_entity_ang_use_side", "0", true)
+local delta_y = 0
+local previous_delta_y = 0
+
+
+local rolling = false
+local initial_roll = 0
+local initial_roll_x = 0
+local current_x = 0
+local roll_x = 0
+local start_x = 0
+local previous_roll = 0
+local roll_x_delta = 0
+local roll_release_time = 0
+
+
+local pitch_limit = 90
local function CalcDrag()
+ if not pace.properties or not pace.properties.search then return end
if
pace.BusyWithProperties:IsValid() or
- pace.ActiveSpecialPanel:IsValid() or
+ (pace.ActiveSpecialPanel:IsValid() and not pace.ActiveSpecialPanel.ignore_saferemovespecialpanel) or
pace.editing_viewmodel or
pace.editing_hands or
pace.properties.search:HasFocus()
@@ -161,7 +320,7 @@ local function CalcDrag()
local ftime = FrameTime() * 50
local mult = 5
- if input.IsKeyDown(KEY_LCONTROL) or input.IsKeyDown(KEY_RCONTROL) then
+ if MovementBindDown("slow") then
mult = 0.1
end
@@ -197,12 +356,60 @@ local function CalcDrag()
mult = mult * math.min(origin:Distance(pace.ViewPos) / 200, 3)
- if input.IsKeyDown(KEY_LSHIFT) then
+ if MovementBindDown("speed") then
mult = mult + 5
end
+ if MovementBindDown("roll_drag") then
+ local current_x,current_y = input.GetCursorPos()
+ if not rolling then
+ start_x,_ = input.GetCursorPos()
+ rolling = true
+ initial_roll = previous_roll
+ else
+ local wrapping = false
+ local x,_ = input.GetCursorPos()
+ if x >= ScrW()-1 then
+ input.SetCursorPos(2,current_y)
+ current_x = 2
+ start_x = start_x - ScrW() + 2
+ wrapping = true
+ end
+ if x <= 1 then
+ wrapping = true
+ input.SetCursorPos(ScrW()-2,current_y)
+ current_x = ScrW() - 2
+ start_x = start_x + ScrW() - 2
+ wrapping = true
+ end
+
+ local snap = pace.roll_snapping:GetFloat()
+ roll_x_delta = x - start_x --current delta (modify)
+ if not wrapping then
+ pace.view_roll = (180 + math.Round(200 * (initial_roll + x - start_x) / ScrW(),2)) % 360 - 180
+ pace.FlashNotification("view roll : " .. pace.view_roll .. " degrees (Ctrl to snap by " .. snap .. " degrees)")
+ end
+
+ if snap ~= 0 and input.IsButtonDown(KEY_LCONTROL) and pace.view_roll ~= nil then
+ pace.view_roll = math.Round(pace.view_roll / snap,0) * snap
+ pace.FlashNotification("view roll : " .. pace.view_roll .. " (snapped to nearest " .. snap .. " degrees)")
+ end
+ --will be applied post
+ end
+ elseif rolling then
+ local x,_ = input.GetCursorPos()
+ previous_roll = initial_roll + roll_x_delta
+ if math.abs(start_x - x) < 5 then
+ pace.FlashNotification("view roll reset")
+ pace.view_roll = nil
+ previous_roll = 0 initial_roll = 0 start_x = 0 roll_x_delta = 0
+ end
+ rolling = false
+ end
+
if not pace.IsSelecting then
if mcode == MOUSE_LEFT then
+ pace.dragging = true
local mpos = Vector(input.GetCursorPos())
if mpos.x >= ScrW() - 1 then
@@ -211,48 +418,99 @@ local function CalcDrag()
mpos = set_mouse_pos(ScrW() - 2, gui.MouseY())
end
+ local overflows = false
if mpos.y >= ScrH() - 1 then
mpos = set_mouse_pos(gui.MouseX(), 1)
+ overflows = 1
elseif mpos.y < 1 then
mpos = set_mouse_pos(gui.MouseX(), ScrH() - 2)
+ overflows = -1
end
local delta = (held_mpos - mpos) / 5 * math.rad(pace.ViewFOV)
- pace.ViewAngles.p = math.Clamp(held_ang.p - delta.y, -90, 90)
+ pace.ViewAngles.p = math.Clamp(held_ang.p - delta.y, -pitch_limit, pitch_limit)
pace.ViewAngles.y = held_ang.y + delta.x
+ if pace.viewlock and pace.viewlock_mode == "zero pitch" then
+ delta_y = (held_ang.p - delta.y)
+ if (previous_delta_y ~= delta_y) and (not overflows) then
+ pace.ViewPos = pace.ViewPos + Vector(0,0,delta_y - previous_delta_y) * pace.viewlock_distance / 300
+ elseif overflows then
+ pace.ViewPos = pace.ViewPos + Vector(0,0,overflows) * pace.viewlock_distance / 300
+ end
+ previous_delta_y = (held_ang.p - delta.y)
+ end
+ else
+ previous_delta_y = 0
+ delta_y = 0
+ pace.dragging = false
end
end
- if input.IsKeyDown(KEY_W) then
- pace.ViewPos = pace.ViewPos + pace.ViewAngles:Forward() * mult * ftime
- elseif input.IsKeyDown(KEY_S) then
- pace.ViewPos = pace.ViewPos - pace.ViewAngles:Forward() * mult * ftime
- end
-
- if input.IsKeyDown(KEY_D) then
- pace.ViewPos = pace.ViewPos + pace.ViewAngles:Right() * mult * ftime
- elseif input.IsKeyDown(KEY_A) then
- pace.ViewPos = pace.ViewPos - pace.ViewAngles:Right() * mult * ftime
- end
+ local viewlock_direct = (pace.viewlock and not pace.dragging) and (pace.viewlock_mode == "direct")
+ if pace.delaymovement < RealTime() then
+ if MovementBindDown("forward") then
+ pace.ViewPos = pace.ViewPos + pace.ViewAngles:Forward() * mult * ftime
+ if pace.viewlock or follow_entity_ang:GetBool() then
+ pace.viewlock_distance = pace.viewlock_distance - mult * ftime
+ end
+ elseif MovementBindDown("back") then
+ pace.ViewPos = pace.ViewPos - pace.ViewAngles:Forward() * mult * ftime
+ if pace.viewlock or follow_entity_ang:GetBool()then
+ pace.viewlock_distance = pace.viewlock_distance + mult * ftime
+ end
+ end
- if input.IsKeyDown(KEY_SPACE) then
- if not IsValid(pace.timeline.frame) then
- pace.ViewPos = pace.ViewPos + pace.ViewAngles:Up() * mult * ftime
+ if MovementBindDown("moveright") then
+ pace.ViewPos = pace.ViewPos + pace.ViewAngles:Right() * mult * ftime
+ elseif MovementBindDown("moveleft") then
+ pace.ViewPos = pace.ViewPos - pace.ViewAngles:Right() * mult * ftime
end
- end
- --[[if input.IsKeyDown(KEY_LALT) then
- pace.ViewPos = pace.ViewPos + pace.ViewAngles:Up() * -mult * ftime
- end]]
+ if MovementBindDown("up") then
+ if not IsValid(pace.timeline.frame) then
+ if viewlock_direct then
+ local up = pace.ViewAngles:Up()
+ mult = mult * up.z
+ end
+ pace.ViewPos = pace.ViewPos + pace.ViewAngles:Up() * mult * ftime
+ end
+ elseif MovementBindDown("down") then
+ if not IsValid(pace.timeline.frame) then
+ if viewlock_direct then
+ local up = pace.ViewAngles:Up()
+ mult = mult * up.z
+ end
+ pace.ViewPos = pace.ViewPos - pace.ViewAngles:Up() * mult * ftime
+ end
+ end
+ if viewlock_direct and pace.viewlock_mode ~= "frame of reference" then
+ local distance = pace.viewlock_distance or 75
+ pace.ViewAngles = (pace.viewlock_pos - pace.ViewPos):Angle()
+ local newpos = pace.viewlock_pos - distance * pace.ViewAngles:Forward()
+ pace.ViewPos = newpos
+ end
+ end
end
local follow_entity = CreateClientConVar("pac_camera_follow_entity", "0", true)
local enable_editor_view = CreateClientConVar("pac_enable_editor_view", "1", true)
+cvars.AddChangeCallback("pac_enable_editor_view", function(name, old, new)
+ if new == "1" then
+ pace.EnableView(true)
+ else
+ pace.CameraPartSwapView()
+ pac.RemoveHook("CalcView", "editor")
+ end
+end, "pace_update_editor_view")
+
local lastEntityPos
+pace.view_reversed = 1
+pace.viewlock_distance = 75
function pace.CalcView(ply, pos, ang, fov)
+ if not pace.IsActive() then pace.EnableView(false) return end
if pace.editing_viewmodel or pace.editing_hands then
pace.ViewPos = pos
pace.ViewAngles = ang
@@ -269,8 +527,127 @@ function pace.CalcView(ply, pos, ang, fov)
lastEntityPos = nil
end
+ if follow_entity_ang:GetBool() then
+ local ent = pace.GetViewEntity()
+ local ang = ent:GetAngles()
+ if follow_entity_ang_side:GetBool() then ang = ang:Right():Angle() end
+ local pos = ent:GetPos() + ent:OBBCenter()
+ pace.viewlock = nil
+ pace.viewlock_pos = pos
+ pace.viewlock_pos_deltaZ = pace.viewlock_pos
+ pace.viewlock_distance = pace.viewlock_distance or 75
+ if pace.viewlock_distance > 10 then
+ pace.ViewAngles = (pace.view_reversed * ang:Forward()):Angle()
+ local newpos = pos - pace.viewlock_distance*pace.ViewAngles:Forward()
+ pace.ViewPos = newpos
+ else
+ pace.view_reversed = -pace.view_reversed --this will flip between front-facing and back-facing
+ pace.viewlock_distance = 75 --but for that to happen we need to move the imposed position forward
+ pace.delaymovement = RealTime() + 0.5
+ pace.ViewPos = pace.ViewPos + 75*pace.ViewAngles:Forward()
+ end
+ end
+
local pos, ang, fov = pac.CallHook("EditorCalcView", pace.ViewPos, pace.ViewAngles, pace.ViewFOV)
+ if pace.viewlock then
+ local pitch = pace.ViewAngles.p
+ local viewlock_pos
+ if isvector(pace.viewlock) then
+ viewlock_pos = pace.viewlock
+ elseif isentity(pace.viewlock) then
+ viewlock_pos = pace.viewlock:GetPos() + pace.viewlock:OBBCenter()
+ elseif pace.viewlock.GetWorldPosition then
+ viewlock_pos = pace.viewlock:GetWorldPosition()
+ end
+
+ pace.viewlock_pos = viewlock_pos
+ ang = ang or pace.ViewAngles
+ pos = pos or pace.ViewPos
+ local deltaZ = Vector(0,0,pace.ViewPos.z - viewlock_pos.z)
+ pace.viewlock_pos_deltaZ = pace.viewlock_pos + deltaZ
+
+
+ if pace.viewlock_distance < 10 then
+ pace.view_reversed = -pace.view_reversed --this will flip between front-facing and back-facing
+ pace.viewlock_distance = 75 --but for that to happen we need to move the imposed position forward
+ pos = pace.ViewPos - 75*pace.ViewAngles:Forward()
+ end
+
+ if pace.viewlock_mode == "free pitch" then
+ pitch_limit = 90
+ viewlock_pos = viewlock_pos + deltaZ
+ local distance = pace.viewlock_distance or viewlock_pos:Distance(pace.ViewPos)
+ if not pace.dragging then
+ ang = (-pace.ViewPos + viewlock_pos):Angle()
+ ang:Normalize()
+ pos = viewlock_pos - pace.view_reversed * distance * ang:Forward()
+ else
+ ang = (-pace.ViewPos + viewlock_pos):Angle()
+ ang:Normalize()
+ end
+
+ ang.p = pitch
+ elseif pace.viewlock_mode == "zero pitch" then
+ viewlock_pos = viewlock_pos + deltaZ
+ local distance = pace.viewlock_distance or viewlock_pos:Distance(pace.ViewPos)
+ if not pace.dragging then
+ ang = (-pace.ViewPos + viewlock_pos):Angle()
+ ang:Normalize()
+ pos = viewlock_pos - pace.view_reversed * distance * ang:Forward()
+ else
+ ang = (-pace.ViewPos + viewlock_pos):Angle()
+ ang:Normalize()
+ end
+
+ ang.p = 0
+ elseif pace.viewlock_mode == "direct" then
+ pitch_limit = 89.9
+ local distance = pace.viewlock_distance or viewlock_pos:Distance(pace.ViewPos)
+ local newpos
+ if pace.dragging then
+ newpos = viewlock_pos - distance * pace.ViewAngles:Forward()
+ pos = newpos
+ ang = (-newpos + pace.viewlock_pos):Angle()
+ ang:Normalize()
+ pace.ViewAngles = ang
+ else
+ newpos = viewlock_pos + pace.view_reversed * distance * pace.ViewAngles:Forward()
+ ang = (-pace.ViewPos + newpos):Angle()
+ ang:Normalize()
+ end
+ elseif pace.viewlock_mode == "frame of reference" then
+ pitch_limit = 90
+ viewlock_pos = viewlock_pos + deltaZ
+ local distance = pace.viewlock_distance or viewlock_pos:Distance(pace.ViewPos)
+ if pace.viewlock and pace.viewlock.GetDrawPosition then
+ local _pos, _ang = pace.viewlock:GetDrawPosition()
+ local mat = Matrix()
+ mat:Rotate(_ang)
+ if pace.viewlock_axis == "x" then
+ --mat:Scale(Vector(-1,1,1))
+ elseif pace.viewlock_axis == "y" then
+ mat:Rotate(Angle(0,90,0))
+ elseif pace.viewlock_axis == "z" then
+ mat:Rotate(Angle(90,0,0))
+ end
+ mat:Scale(Vector(pace.view_reversed,1,1))
+ ang = mat:GetAngles()
+ ang.r = pace.view_reversed*ang.r
+ pos = _pos - distance * ang:Forward()
+ end
+ elseif pace.viewlock_mode == "disable" then
+ pitch_limit = 90
+ pace.viewlock = nil
+ end
+ --we apply the reversion only once, so reset here
+ if pace.view_reversed == -1 and (pace.viewlock_mode ~= "frame of reference") then
+ pace.view_reversed = 1
+ end
+ else
+ pitch_limit = 90
+ end
+
if pos then
pace.ViewPos = pos
end
@@ -282,13 +659,50 @@ function pace.CalcView(ply, pos, ang, fov)
if fov then
pace.ViewFOV = fov
end
+
+ local viewang_final = Angle(pace.ViewAngles)
+
+ if pace.view_roll then
+ pace.ViewAngles_postRoll = Angle(viewang_final)
+ pace.ViewAngles_postRoll:RotateAroundAxis(pace.ViewAngles:Forward(), pace.view_roll)
+ viewang_final = pace.ViewAngles_postRoll
+ end
- return
- {
- origin = pace.ViewPos,
- angles = pace.ViewAngles,
- fov = pace.ViewFOV,
- }
+
+ --[[
+ local entpos = pace.GetViewEntity():WorldSpaceCenter()
+ local diff = pace.ViewPos - entpos
+ local MAX_CAMERA_DISTANCE = 300
+ local backtrace = util.QuickTrace(entpos, diff*50000, ply)
+ local final_dist = math.min(diff:Length(), MAX_CAMERA_DISTANCE, (backtrace.HitPos - entpos):Length() - 10)
+ pace.ViewPos = entpos + diff:GetNormalized() * final_dist
+ ]]
+
+ if not pace.camera_orthographic then
+ return
+ {
+ origin = pace.ViewPos,
+ angles = viewang_final,
+ fov = pace.ViewFOV
+ }
+ else
+ local orthoborder = pace.Editor.zoomslider:GetValue() / 1000
+ return
+ {
+ origin = pace.ViewPos,
+ angles = viewang_final,
+ fov = pace.ViewFOV,
+ ortho = {
+ left = -orthoborder * ScrW(),
+ right = orthoborder * ScrW(),
+ top = -orthoborder * ScrH(),
+ bottom = orthoborder * ScrH()
+ },
+ znear = pace.Editor.ortho_nearz:GetValue(),
+ zfar = pace.Editor.ortho_farz:GetValue()
+ }
+ end
+
end
function pace.ShouldDrawLocalPlayer()
@@ -342,8 +756,14 @@ function pace.EnableView(b)
pac.AddHook("GUIMousePressed", "editor", pace.GUIMousePressed)
pac.AddHook("GUIMouseReleased", "editor", pace.GUIMouseReleased)
pac.AddHook("ShouldDrawLocalPlayer", "editor", pace.ShouldDrawLocalPlayer, DLib and -4 or ULib and -1 or nil)
- pac.AddHook("CalcView", "editor", pace.CalcView, DLib and -4 or ULib and -1 or nil)
- pac.RemoveHook("CalcView", "camera_part")
+ if enable_editor_view:GetBool() then
+ pac.AddHook("CalcView", "editor", pace.CalcView, DLib and -4 or ULib and -1 or nil)
+ pac.RemoveHook("CalcView", "camera_part")
+ pac.active_camera = nil
+ else
+ if pac.HasRemainingCameraPart() then pace.CameraPartSwapView() end
+ pac.RemoveHook("CalcView", "editor")
+ end
pac.AddHook("HUDPaint", "editor", pace.HUDPaint)
pac.AddHook("HUDShouldDraw", "editor", pace.HUDShouldDraw)
pac.AddHook("PostRenderVGUI", "editor", pace.PostRenderVGUI)
@@ -355,36 +775,69 @@ function pace.EnableView(b)
pac.RemoveHook("GUIMouseReleased", "editor")
pac.RemoveHook("ShouldDrawLocalPlayer", "editor")
pac.RemoveHook("CalcView", "editor")
- pac.RemoveHook("CalcView", "camera_part")
+ pac.AddHook("CalcView", "camera_part", pac.HandleCameraPart)
pac.RemoveHook("HUDPaint", "editor")
pac.RemoveHook("HUDShouldDraw", "editor")
pac.RemoveHook("PostRenderVGUI", "editor")
pace.SetTPose(false)
end
+end
- if not enable_editor_view:GetBool() then
- local ply = LocalPlayer()
- pac.RemoveHook("CalcView", "editor")
- pac.AddHook("CalcView", "camera_part", function(ply, pos, ang, fov, nearz, farz)
- for _, part in pairs(pac.GetLocalParts()) do
- if part:IsValid() and part.ClassName == "camera" then
- part:CalcShowHide()
- local temp = {}
- if not part:IsHidden() then
- pos, ang, fov, nearz, farz = part:CalcView(_,_,ply:EyeAngles())
- temp.origin = pos
- temp.angles = ang
- temp.fov = fov
- temp.znear = nearz
- temp.zfar = farz
- temp.drawviewer = not part.DrawViewModel
- return temp
- end
+function pace.ManuallySelectCamera(obj, doselect)
+ if obj and doselect then
+ obj:CameraTakePriority(true)
+ pace.CameraPartSwapView(true)
+ pac.active_camera_manual = obj
+ elseif not doselect then
+ for i,v in pairs(pac.GetLocalParts()) do
+ if v.ClassName == "camera" then
+ if not v:IsHidden() and v ~= obj then
+ v:CameraTakePriority(true)
+ pace.CameraPartSwapView(true)
+ pac.active_camera_manual = v
+ return
end
end
- end)
- --pac.RemoveHook("ShouldDrawLocalPlayer", "editor")
+ end
+ pac.active_camera_manual = nil
+ else
+ for i,v in pairs(pac.GetLocalParts()) do
+ if v.ClassName == "camera" then
+ if not v:IsHidden() then
+ v:CameraTakePriority(true)
+ pace.CameraPartSwapView(true)
+ pac.active_camera_manual = v
+ return
+ end
+ end
+ end
+ end
+end
+
+function pace.CameraPartSwapView(force_pac_camera)
+ local pac_camera_parts_should_override = not enable_editor_view:GetBool() or not pace.Editor:IsValid() or pac.HasRemainingCameraPart()
+
+ if pace.Editor:IsValid() and enable_editor_view:GetBool() and not force_pac_camera then pac_camera_parts_should_override = false end
+
+ if pac.HandleCameraPart() == nil then --no cameras
+ if not pace.ShouldDrawLocalPlayer() then
+ pace.EnableView(false)
+ end
+ pac.RemoveHook("CalcView", "camera_part")
+ elseif pac_camera_parts_should_override then --cameras
+ pac.AddHook("CalcView", "camera_part", pac.HandleCameraPart)
+ pac.RemoveHook("CalcView", "editor")
+ else
+ pace.EnableView(enable_editor_view:GetBool())
+ --[[if not GetConVar("pac_copilot_force_preview_cameras"):GetBool() then
+
+ else
+ pace.EnableView(false)
+ end]]
end
+
+
+ return pac.active_camera
end
local function CalcAnimationFix(ent)
@@ -556,7 +1009,7 @@ end
function pace.GetBreathing()
return pace.breathing
end
-function pace.ResetEyeAngles()
+function pace.ResetEyeAngles(pitch_only)
local ent = pace.GetViewEntity()
if ent:IsValid() then
if ent:IsPlayer() then
@@ -572,7 +1025,13 @@ function pace.ResetEyeAngles()
end)
end)
- ent:SetEyeAngles(Angle(0, 0, 0))
+ if not pitch_only then
+ ent:SetEyeAngles(Angle(0, 0, 0))
+ else
+ local ang = ent:EyeAngles()
+ ang.p = 0
+ ent:SetEyeAngles(ang)
+ end
else
ent:SetAngles(Angle(0, 0, 0))
end
@@ -580,3 +1039,54 @@ function pace.ResetEyeAngles()
pac.SetupBones(ent)
end
end
+
+function pace.PopupMiniFOVSlider()
+ zoom_persistent = GetConVar("pac_zoom_persistent")
+ zoom_smooth = GetConVar("pac_zoom_smooth")
+ local zoomframe = vgui.Create( "DPanel" )
+ local x,y = input.GetCursorPos()
+ zoomframe:SetPos(x - 90,y - 10)
+ zoomframe:SetSize( 180, 20 )
+
+ zoomframe.zoomslider = vgui.Create("DNumSlider", zoomframe)
+ zoomframe.zoomslider:DockPadding(4,0,0,0)
+ zoomframe.zoomslider:SetSize(200, 20)
+ zoomframe.zoomslider:SetDecimals( 0 )
+ zoomframe.zoomslider:SetText("Camera FOV")
+ if pace.camera_orthographic then
+ zoomframe.zoomslider:SetText("Ortho. Width")
+ end
+ pace.RefreshZoomBounds(zoomframe.zoomslider)
+ zoomframe.zoomslider:SetDark(true)
+ zoomframe.zoomslider:SetDefaultValue( 75 )
+
+ zoomframe.zoomslider:SetValue( pace.ViewFOV )
+
+ function zoomframe:Think(...)
+ pace.ViewFOV = zoomframe.zoomslider:GetValue()
+ if zoom_smooth:GetInt() == 1 then
+ pace.SetZoom(zoomframe.zoomslider:GetValue(),true)
+ else
+ pace.SetZoom(zoomframe.zoomslider:GetValue(),false)
+ end
+ end
+
+ local hook_id = "pac_tools_menu"..util.SHA256(tostring(zoomframe))
+
+ pac.AddHook("VGUIMousePressed", hook_id, function(pnl, code)
+ pace.OverridingFOVSlider = true --to link the values with the original panel in the pac editor panel
+ if not IsValid(zoomframe) then
+ pac.RemoveHook("VGUIMousePressed", hook_id)
+ return
+ end
+ if code == MOUSE_LEFT or code == MOUSE_RIGHT then
+ if not zoomframe:IsOurChild(pnl) then
+ if zoomframe.zoomslider then zoomframe.zoomslider:Remove() end
+ zoomframe:Remove()
+ pac.RemoveHook("VGUIMousePressed", hook_id)
+ pace.OverridingFOVSlider = false
+ end
+ end
+ end)
+
+end
diff --git a/lua/pac3/editor/client/wear.lua b/lua/pac3/editor/client/wear.lua
index cb2c202d5..8a03a5d73 100644
--- a/lua/pac3/editor/client/wear.lua
+++ b/lua/pac3/editor/client/wear.lua
@@ -46,21 +46,27 @@ function pace.ClearParts()
end)
end
-
+--wearing tracker counter
+pace.still_loading_wearing_count = 0
+--reusable function
+function pace.ExtendWearTracker(duration)
+ if not duration or not isnumber(duration) then duration = 1 end
+ pace.still_loading_wearing = true
+ pace.still_loading_wearing_count = pace.still_loading_wearing_count + 1 --this group is added to the tracked wear count
+ timer.Simple(duration, function()
+ pace.still_loading_wearing_count = pace.still_loading_wearing_count - 1 --assume max 8 seconds to wear
+ if pace.still_loading_wearing_count == 0 then --if this is the last group to wear, we're done
+ pace.still_loading_wearing = false
+ end
+ end)
+end
do -- to server
local function net_write_table(tbl)
-
local buffer = pac.StringStream()
buffer:writeTable(tbl)
-
local data = buffer:getString()
- local ok, err = pcall(net.WriteStream, data)
-
- if not ok then
- return ok, err
- end
-
+ net.WriteStream(data)
return #data
end
@@ -76,6 +82,12 @@ do -- to server
local data = {part = part:ToTable()}
+ --hack so that camera part doesn't force-gain focus if it's not manually created, because wearing removes and re-creates parts.
+ pace.hack_camera_part_donot_treat_wear_as_creating_part = true
+ timer.Simple(2, function()
+ pace.hack_camera_part_donot_treat_wear_as_creating_part = nil
+ end)
+
if extra then
table.Merge(data, extra)
end
@@ -85,15 +97,18 @@ do -- to server
net.Start("pac_submit")
- local bytes, err = net_write_table(data)
+ local ok, bytes = pcall(net_write_table, data)
- if not bytes then
- pace.Notify(false, "unable to transfer data to server: " .. tostring(err or "too big"), pace.pac_show_uniqueid:GetBool() and string.format("%s (%s)", part:GetName(), part:GetPrintUniqueID()) or part:GetName())
+ if not ok then
+ net.Abort()
+ pace.Notify(false, "unable to transfer data to server: " .. tostring(bytes or "too big"), pace.pac_show_uniqueid:GetBool() and string.format("%s (%s)", part:GetName(), part:GetPrintUniqueID()) or part:GetName())
return false
end
net.SendToServer()
- pac.Message(('Transmitting outfit %q to server (%s)'):format(part.Name or part.ClassName or '', string.NiceSize(bytes)))
+ pac.Message(("Transmitting outfit %q to server (%s)"):format(part.Name or part.ClassName or "", string.NiceSize(bytes)))
+
+ pace.ExtendWearTracker(8)
return true
end
@@ -106,9 +121,10 @@ do -- to server
end
net.Start("pac_submit")
- local ret,err = net_write_table(data)
- if ret == nil then
- pace.Notify(false, "unable to transfer data to server: "..tostring(err or "too big"), name)
+ local ok, err = pcall(net_write_table, data)
+ if not ok then
+ net.Abort()
+ pace.Notify(false, "unable to transfer data to server: " .. tostring(err or "too big"), name)
return false
end
net.SendToServer()
@@ -161,11 +177,11 @@ do -- from server
end
if owner == pac.LocalPlayer then
- pace.CallHook("OnWoreOutfit", part)
+ pac.CallHook("OnWoreOutfit", part)
end
- part:CallRecursive('OnWorn')
- part:CallRecursive('PostApplyFixes')
+ part:CallRecursive("OnWorn")
+ part:CallRecursive("PostApplyFixes")
if part.UpdateOwnerName then
part:UpdateOwnerName(true)
@@ -208,13 +224,20 @@ function pace.HandleReceiveData(data, doitnow)
elseif T == "string" then
return pace.RemovePartFromServer(data.owner, data.part, data)
else
- ErrorNoHalt("PAC: Unhandled "..T..'!?\n')
+ ErrorNoHalt("PAC: Unhandled " .. T .. "!?\n")
end
end
net.Receive("pac_submit", function()
if not pac.IsEnabled() then return end
+ local owner = net.ReadEntity()
+ if owner:IsValid() and owner:IsPlayer() then
+ pac.Message("Receiving outfit from ", owner)
+ else
+ return
+ end
+
net.ReadStream(ply, function(data)
if not data then
pac.Message("message from server timed out")
@@ -222,10 +245,14 @@ net.Receive("pac_submit", function()
end
local buffer = pac.StringStream(data)
- local data = buffer:readTable()
+ local ok, data = pcall(buffer.readTable, buffer)
+ if not ok then
+ pac.Message("received invalid message from server!?")
+ return
+ end
if type(data.owner) ~= "Player" or not data.owner:IsValid() then
- pac.Message("received message from server but owner is not valid!? typeof " .. type(data.owner) .. ' || ', data.owner)
+ pac.Message("received message from server but owner is not valid!? typeof " .. type(data.owner) .. " || ", data.owner)
return
end
@@ -241,9 +268,9 @@ function pace.Notify(allowed, reason, name)
name = name or "???"
if allowed == true then
- pac.Message(string.format('Your part %q has been applied', name))
+ pac.Message(string.format("Your part %q has been applied", name))
else
- chat.AddText(Color(255, 255, 0), "[PAC3] ", Color(255, 0, 0), string.format('The server rejected applying your part (%q) - %s', name, reason))
+ chat.AddText(Color(255, 255, 0), "[PAC3] ", Color(255, 0, 0), string.format("The server rejected applying your part (%q) - %s", name, reason))
end
end
@@ -253,26 +280,93 @@ end)
do
local function LoadUpDefault()
- if next(pac.GetLocalParts()) then
- pac.Message("not wearing autoload outfit, already wearing something")
- elseif pace.IsActive() then
- pac.Message("not wearing autoload outfit, editor is open")
+ if not GetConVar("pac_prompt_for_autoload"):GetBool() then
+ --legacy behavior
+ if next(pac.GetLocalParts()) then
+ pac.Message("not wearing autoload outfit, already wearing something")
+ elseif pace.IsActive() then
+ pac.Message("not wearing autoload outfit, editor is open")
+ else
+ local autoload_file = "autoload"
+ local autoload_result = hook.Run("PAC3Autoload", autoload_file)
+
+ if autoload_result ~= false then
+ if isstring(autoload_result) then
+ autoload_file = autoload_result
+ end
+ pac.Message("Wearing " .. autoload_file .. "...")
+ pace.LoadParts(autoload_file)
+ pace.WearParts()
+ end
+ end
+
else
- local autoload_file = "autoload"
- local autoload_result = hook.Run("PAC3Autoload", autoload_file)
-
- if autoload_result ~= false then
- if isstring(autoload_result) then
- autoload_file = autoload_result
+ --prompt
+ local backup_files, directories = file.Find( "pac3/__backup/*.txt", "DATA", "datedesc")
+ local latest_outfit = cookie.GetString( "pac_last_loaded_outfit", "" )
+ if not backup_files then
+ local pnl = Derma_Query("Do you want to load your autoload outfit?", "PAC3 autoload (pac_prompt_for_autoload)",
+ "load pac3/autoload.txt : " .. string.NiceSize(file.Size("pac3/autoload.txt", "DATA")), function()
+ pac.Message("Wearing autoload...")
+ pace.LoadParts("autoload")
+ pace.WearParts()
+ end,
+
+ "load latest outfit : pac3/" .. latest_outfit .. " " .. string.NiceSize(file.Size("pac3/" .. latest_outfit, "DATA")), function()
+
+ if latest_outfit and file.Exists("pac3/" .. latest_outfit, "DATA") then
+ pac.Message("Wearing latest outfit...")
+ pace.LoadParts(latest_outfit, true)
+ pace.WearParts()
+ end
+ end,
+
+ "cancel", function() pac.Message("Not loading autoload or backups...") end
+ )
+ pnl.Think = function() if not pnl:HasFocus() or (input.IsMouseDown(MOUSE_LEFT) and not (pnl:IsHovered() or pnl:IsChildHovered())) then pnl:Remove() end end
+ else
+ if backup_files[1] then
+ local latest_autosave = "pac3/__backup/" .. backup_files[1]
+ local pnl = Derma_Query("Do you want to load an outfit?", "PAC3 autoload (pac_prompt_for_autoload)",
+ "load pac3/autoload.txt : " .. string.NiceSize(file.Size("pac3/autoload.txt", "DATA")), function()
+ pac.Message("Wearing autoload...")
+ pace.LoadParts("autoload")
+ pace.WearParts()
+ end,
+
+ "load latest backup : " .. latest_autosave .. " " .. string.NiceSize(file.Size(latest_autosave, "DATA")), function()
+ pac.Message("Wearing latest backup outfit...")
+ pace.LoadParts("__backup/" .. backup_files[1], true)
+ pace.WearParts()
+ end,
+
+ "load latest outfit : pac3/" .. latest_outfit .. " " .. string.NiceSize(file.Size("pac3/" .. latest_outfit, "DATA")), function()
+ if latest_outfit and file.Exists("pac3/" .. latest_outfit, "DATA") then
+ pac.Message("Wearing latest outfit...")
+ pace.LoadParts(latest_outfit, true)
+ pace.WearParts()
+ end
+ end,
+
+ "cancel", function() pac.Message("Not loading autoload or backups...") end
+ )
+ pnl.Think = function() if not pnl:HasFocus() or (input.IsMouseDown(MOUSE_LEFT) and not (pnl:IsHovered() or pnl:IsChildHovered())) then pnl:Remove() end end
+ else
+ local pnl = Derma_Query("Do you want to load your autoload outfit?", "PAC3 autoload (pac_prompt_for_autoload)",
+ "load pac3/autoload.txt : " .. string.NiceSize(file.Size("pac3/autoload.txt", "DATA")), function()
+ pac.Message("Wearing autoload...")
+ pace.LoadParts("autoload")
+ pace.WearParts()
+ end,
+
+ "cancel", function() pac.Message("Not loading autoload or backups...") end
+ )
+ pnl.Think = function() if not pnl:HasFocus() or (input.IsMouseDown(MOUSE_LEFT) and not (pnl:IsHovered() or pnl:IsChildHovered())) then pnl:Remove() end end
end
-
- pac.Message("Wearing " .. autoload_file .. "...")
- pace.LoadParts(autoload_file)
- pace.WearParts()
end
end
- pac.RemoveHook("Think", "pac_request_outfits")
+ pac.RemoveHook("Think", "request_outfits")
pac.Message("Requesting outfits in 8 seconds...")
timer.Simple(8, function()
@@ -282,14 +376,14 @@ do
end
local function Initialize()
- pac.RemoveHook("KeyRelease", "pac_request_outfits")
+ pac.RemoveHook("KeyRelease", "request_outfits")
if not pac.LocalPlayer:IsValid() then
return
end
if not pac.IsEnabled() then
- pac.RemoveHook("Think", "pac_request_outfits")
+ pac.RemoveHook("Think", "request_outfits")
pace.NeverLoaded = true
return
end
@@ -297,7 +391,7 @@ do
LoadUpDefault()
end
- hook.Add("pac_Enable", "pac_LoadUpDefault", function()
+ pac.AddHook("pac_Enable", "LoadUpDefault", function()
if not pace.NeverLoaded then return end
pace.NeverLoaded = nil
LoadUpDefault()
@@ -305,22 +399,20 @@ do
local frames = 0
- pac.AddHook("Think", "pac_request_outfits", function()
+ pac.AddHook("Think", "request_outfits", function()
if RealFrameTime() > 0.2 then -- lag?
return
end
frames = frames + 1
- if frames > 400 then
- if not xpcall(Initialize, ErrorNoHalt) then
- pac.RemoveHook("Think", "pac_request_outfits")
- pace.NeverLoaded = true
- end
+ if frames > 400 and not xpcall(Initialize, ErrorNoHalt) then
+ pac.RemoveHook("Think", "request_outfits")
+ pace.NeverLoaded = true
end
end)
- pac.AddHook("KeyRelease", "pac_request_outfits", function()
+ pac.AddHook("KeyRelease", "request_outfits", function()
local me = pac.LocalPlayer
if me:IsValid() and me:GetVelocity():Length() > 50 then
diff --git a/lua/pac3/editor/client/wear_filter.lua b/lua/pac3/editor/client/wear_filter.lua
index 52fb3dc65..7fa9b805b 100644
--- a/lua/pac3/editor/client/wear_filter.lua
+++ b/lua/pac3/editor/client/wear_filter.lua
@@ -1,18 +1,46 @@
local list_form = include("panels/list.lua")
local L = pace.LanguageString
-local cache = {}
+local path = "pac3_config/"
+
+-- old path migration
+pac.AddHook("PrePACEditorOpen", "wear_filter_config_migration", function()
+ if file.IsDir("pac/config", "DATA") and cookie.GetString("pac3_config_migration_dismissed", "0") == "0" then
+ Derma_Query(
+ L "Do you want to migrate the old pac/config folder to pac3_config?",
+ L "Old Config Folder Detected",
+ L "Yes",
+ function()
+ local files, _ = file.Find("pac/config/*.json", "DATA")
+ for i = 1, #files do
+ local f = files[i]
+ local content = file.Read("pac/config/" .. f, "DATA")
+ file.Write(path .. f, content)
+ file.Delete("pac/config/" .. f, "DATA")
+ end
+ file.Delete("pac/config", "DATA")
+ file.Delete("pac", "DATA")
+ end,
+ L "No",
+ function()
+ cookie.Set("pac3_config_migration_dismissed", "1")
+ end
+ )
+ end
+end)
-local function store_config(id, tbl)
- file.CreateDir("pac/config")
- file.Write("pac/config/"..id..".json", util.TableToJSON(tbl))
- cache[id] = tbl
-end
+local cache = setmetatable({}, {__index = function(self, key)
+ local val = util.JSONToTable(file.Read(path .. key .. ".json", "DATA") or "{}") or {}
+ self[key] = val
+ return self
+end})
-local function read_config(id)
- local tbl = util.JSONToTable(file.Read("pac/config/"..id..".json", "DATA") or "{}") or {}
+local function store_config(id, tbl)
+ if not file.IsDir(path, "DATA") then
+ file.CreateDir(path)
+ end
+ file.Write(path .. id .. ".json", util.TableToJSON(tbl))
cache[id] = tbl
- return tbl
end
local function get_config_value(id, key)
@@ -83,7 +111,7 @@ do
end
end
- table.insert(ent.pac_ignored_callbacks, {callback = callback, index = index})
+ table.insert(ent.pac_ignored_callbacks, { callback = callback, index = index })
end
function pac.CleanupEntityIgnoreBound(ent)
@@ -194,7 +222,7 @@ local function generic_form(help)
local pnl = vgui.Create("DListLayout")
local label = pnl:Add("DLabel")
- label:DockMargin(0,5,0,5)
+ label:DockMargin(0, 5, 0, 5)
label:SetWrap(true)
label:SetDark(true)
label:SetAutoStretchVertical(true)
@@ -207,18 +235,18 @@ local function player_list_form(name, id, help)
local pnl = vgui.Create("DListLayout")
local label = pnl:Add("DLabel")
- label:DockMargin(0,5,0,5)
+ label:DockMargin(0, 5, 0, 5)
label:SetWrap(true)
label:SetDark(true)
label:SetAutoStretchVertical(true)
label:SetText(help)
list_form(pnl, name, {
- empty_message = L"No players online.",
+ empty_message = L "No players online.",
name_left = "players",
populate_left = function()
- local blacklist = read_config(id)
+ local blacklist = cache[id]
local tbl = {}
for _, ply in ipairs(player.GetHumans()) do
@@ -233,7 +261,7 @@ local function player_list_form(name, id, help)
return tbl
end,
store_left = function(kv)
- local tbl = read_config(id)
+ local tbl = cache[id]
tbl[jsonid(kv.value)] = kv.name
store_config(id, tbl)
@@ -245,7 +273,7 @@ local function player_list_form(name, id, help)
name_right = name,
populate_right = function()
local tbl = {}
- for id, nick in pairs(read_config(id)) do
+ for id, nick in pairs(cache[id]) do
local ply = pac.ReverseHash(id:sub(2), "Player")
if ply == pac.LocalPlayer then continue end
@@ -264,7 +292,7 @@ local function player_list_form(name, id, help)
return tbl
end,
store_right = function(kv)
- local tbl = read_config(id)
+ local tbl = cache[id]
tbl[kv.value] = nil
store_config(id, tbl)
@@ -294,7 +322,7 @@ do
for _, ply in ipairs(player.GetHumans()) do
if ply == pac.LocalPlayer then continue end
- local icon = menu:AddOption(L"wear only for " .. ply:Nick(), function()
+ local icon = menu:AddOption(L "wear only for " .. ply:Nick(), function()
pace.WearParts(ply)
end)
icon:SetImage(pace.MiscIcons.wear)
@@ -307,11 +335,11 @@ function pace.FillWearSettings(pnl)
list:Dock(FILL)
do
- local cat = list:Add(L"wear filter")
- cat.Header:SetSize(40,40)
+ local cat = list:Add(L "wear filter")
+ cat.Header:SetSize(40, 40)
cat.Header:SetFont("DermaLarge")
local list = vgui.Create("DListLayout")
- list:DockPadding(20,20,20,20)
+ list:DockPadding(20, 20, 20, 20)
cat:SetContents(list)
local mode = vgui.Create("DComboBox", list)
@@ -327,13 +355,15 @@ function pace.FillWearSettings(pnl)
end
if value == "steam friends" then
- mode.form = generic_form(L"Only your steam friends can see your worn outfit.")
+ mode.form = generic_form(L "Only your steam friends can see your worn outfit.")
elseif value == "whitelist" then
- mode.form = player_list_form(L"whitelist", "wear_whitelist", L"Only the players in the whitelist can see your worn outfit.")
+ mode.form = player_list_form(L "whitelist", "wear_whitelist",
+ L "Only the players in the whitelist can see your worn outfit.")
elseif value == "blacklist" then
- mode.form = player_list_form( L"blacklist", "wear_blacklist", L"The players in the blacklist cannot see your worn outfit.")
+ mode.form = player_list_form(L "blacklist", "wear_blacklist",
+ L "The players in the blacklist cannot see your worn outfit.")
elseif value == "disabled" then
- mode.form = generic_form(L"Everyone can see your worn outfit.")
+ mode.form = generic_form(L "Everyone can see your worn outfit.")
end
GetConVar("pace_wear_filter_mode"):SetString(value:gsub(" ", "_"))
@@ -347,11 +377,11 @@ function pace.FillWearSettings(pnl)
end
do
- local cat = list:Add(L"outfit filter")
- cat.Header:SetSize(40,40)
+ local cat = list:Add(L "outfit filter")
+ cat.Header:SetSize(40, 40)
cat.Header:SetFont("DermaLarge")
local list = vgui.Create("DListLayout")
- list:DockPadding(20,20,20,20)
+ list:DockPadding(20, 20, 20, 20)
cat:SetContents(list)
local mode = vgui.Create("DComboBox", list)
@@ -367,13 +397,15 @@ function pace.FillWearSettings(pnl)
end
if value == "steam friends" then
- mode.form = generic_form(L"You will only see outfits from your steam friends.")
+ mode.form = generic_form(L "You will only see outfits from your steam friends.")
elseif value == "whitelist" then
- mode.form = player_list_form(L"whitelist", "outfit_whitelist", L"You will only see outfits from the players in the whitelist.")
+ mode.form = player_list_form(L "whitelist", "outfit_whitelist",
+ L "You will only see outfits from the players in the whitelist.")
elseif value == "blacklist" then
- mode.form = player_list_form(L"blacklist", "outfit_blacklist", L"You will see outfits from everyone except the players in the blacklist.")
+ mode.form = player_list_form(L "blacklist", "outfit_blacklist",
+ L "You will see outfits from everyone except the players in the blacklist.")
elseif value == "disabled" then
- mode.form = generic_form(L"You will see everyone's outfits.")
+ mode.form = generic_form(L "You will see everyone's outfits.")
end
GetConVar("pace_outfit_filter_mode"):SetString(value:gsub(" ", "_"))
diff --git a/lua/pac3/editor/client/wires.lua b/lua/pac3/editor/client/wires.lua
index fe36e0ce2..6e95871a0 100644
--- a/lua/pac3/editor/client/wires.lua
+++ b/lua/pac3/editor/client/wires.lua
@@ -123,6 +123,65 @@ local function draw_hermite(x,y, w,h, ...)
DrawHermite(...)
cam.End(cam3d)
end
+
+local function draw_hermite_list(part, tbl, property)
+ for _, part2 in pairs(tbl) do
+ local from = part
+ local to = part2
+ if not to:IsValid() then continue end
+
+ local from_pnl = from.pace_properties[property]
+ local to_pnl = to.pace_tree_node or NULL
+
+ if not from_pnl:IsValid() then continue end
+ if not to_pnl:IsValid() then continue end
+
+ local params = {}
+
+ params["$basetexture"] = to.Icon or "gui/colors.png"
+ params["$vertexcolor"] = 1
+ params["$vertexalpha"] = 1
+ params["$nocull"] = 1
+
+ local path = to_pnl:GetModel()
+ if path then
+ path = "spawnicons/" .. path:sub(1, -5) .. "_32"
+ params["$basetexture"] = path
+ end
+
+ local mat = CreateMaterial("pac_wire_icon_" .. params["$basetexture"], "UnlitGeneric", params)
+
+ render.SetMaterial(mat)
+
+ local fx,fy = from_pnl:LocalToScreen(from_pnl:GetWide(), from_pnl:GetTall() / 2)
+
+ local tx,ty = to_pnl.Icon:LocalToScreen(0,to_pnl.Icon:GetTall() / 2)
+
+ do
+ local x,y = pace.tree:LocalToScreen(0,0)
+ local w,h = pace.tree:LocalToScreen(pace.tree:GetSize())
+
+ tx = math.Clamp(tx, x, w)
+ ty = math.Clamp(ty, y, h)
+ end
+
+ from_pnl.wire_smooth_hover = from_pnl.wire_smooth_hover or 0
+
+ if from_pnl:IsHovered() or (from.pace_tree_node and from.pace_tree_node:IsValid() and from.pace_tree_node.Label:IsHovered()) then
+ from_pnl.wire_smooth_hover = from_pnl.wire_smooth_hover + (5 - from_pnl.wire_smooth_hover) * FrameTime() * 20
+ else
+ from_pnl.wire_smooth_hover = from_pnl.wire_smooth_hover + (0 - from_pnl.wire_smooth_hover) * FrameTime() * 20
+ end
+
+ from_pnl.wire_smooth_hover = math.Clamp(from_pnl.wire_smooth_hover, 0, 5)
+
+ if from_pnl.wire_smooth_hover > 0.01 then
+ draw_hermite(0,0,ScrW(),ScrH(), from_pnl.wire_smooth_hover, fx,fy, tx,ty, Color(255,255,255), Color(255,255,255, 255), 1)
+ end
+ end
+end
+
+
--[[
function PANEL:DrawHermite(...)
local x, y = self:ScreenToLocal(0,0)
@@ -209,4 +268,34 @@ hook.Add("PostRenderVGUI", "beams", function()
end
end
+ if part.ClassName == "proxy" and part.valid_parts_in_expression then
+ draw_hermite_list(part, part.valid_parts_in_expression, "Expression")
+ end
+
+ if part.ExtraHermites then
+ draw_hermite_list(part, part.ExtraHermites, part.ExtraHermites_Property)
+ end
+
+ if pace.selecting_property and pace.IsSelecting and pace.bypass_tree then
+ render.SetMaterial(Material("icon16/add.png"))
+ local tx,ty = input.GetCursorPos()
+ local hovered_pnl = vgui.GetHoveredPanel()
+ if hovered_pnl then
+ local should_draw = false
+ local to_pnl = hovered_pnl
+ local tall = to_pnl:GetTall() / 2
+ if hovered_pnl.Icon then to_pnl = hovered_pnl:GetParent() should_draw = true end
+ if hovered_pnl.ClassName == "pac_dtree_node_button" then to_pnl = hovered_pnl should_draw = true end
+ if should_draw then tx,ty = to_pnl:LocalToScreen(0,tall) end
+ end
+ local fx,fy = pace.selecting_property:LocalToScreen(pace.selecting_property:GetWide(), pace.selecting_property:GetTall() / 2)
+
+ if math.abs(fy-ty) > 40 then
+ local ex, ey = pace.Editor:GetPos()
+ if (tx < (ex + pace.Editor:GetWide())) and (tx > ex) then --mouse inside the editor
+ draw_hermite(0,0,ScrW(),ScrH(), 5, fx,fy, tx,ty, Color(255,255,255), Color(255,255,255, 255), 1)
+ end
+ end
+ end
+
end)
diff --git a/lua/pac3/editor/server/bans.lua b/lua/pac3/editor/server/bans.lua
index 2d3bfdb81..f87a1113c 100644
--- a/lua/pac3/editor/server/bans.lua
+++ b/lua/pac3/editor/server/bans.lua
@@ -1,3 +1,7 @@
+util.AddNetworkString("pac.BanUpdate")
+util.AddNetworkString("pac.RequestBanStates")
+util.AddNetworkString("pac.SendBanStates")
+
local function get_bans()
local str = file.Read("pac_bans.txt", "DATA")
@@ -111,3 +115,47 @@ function pace.IsBanned(ply)
return pace.Bans[ply:UniqueID()] ~= nil
end
+
+net.Receive("pac.BanUpdate", function(len, ply)
+ if not ply:IsAdmin() then
+ return
+ end
+
+ pac.Message("Received ban list update operation from : ", ply)
+ pac.Message("Time : ", os.date( "%a %X %x", os.time() ))
+ local playerlist = net.ReadTable()
+ for i,v in pairs(playerlist) do
+ if playerlist[i] == "Allowed" then
+ pace.Unban(i)
+ elseif playerlist[i] == "Banned" then
+ pace.Ban(i)
+ end
+ print(i, "banned?", pace.IsBanned(i), "Update ->", playerlist[i])
+ end
+end)
+
+net.Receive("pac.RequestBanStates", function(len,ply)
+ if not ply:IsAdmin() then
+ return
+ end
+
+ local archive = net.ReadBool()
+ pac.Message("Received ban list request from : ", ply)
+ pac.Message("Time : ", os.date( "%a %X %x", os.time() ))
+ local players = {}
+ for _,v in pairs(player.GetAll()) do
+ players[v] = false
+ end
+ if not pace.Bans then
+ pace.Bans = get_bans()
+ end
+ for i,v in pairs(pace.Bans) do
+ print(player.GetBySteamID(i), player.GetBySteamID(v[1]))
+ local ply = player.GetBySteamID(v[1])
+ players[ply] = true
+ end
+
+ net.Start("pac.SendBanStates")
+ net.WriteTable(players)
+ net.Send(ply)
+end)
diff --git a/lua/pac3/editor/server/combat_bans.lua b/lua/pac3/editor/server/combat_bans.lua
new file mode 100644
index 000000000..35a9a9b8b
--- /dev/null
+++ b/lua/pac3/editor/server/combat_bans.lua
@@ -0,0 +1,109 @@
+
+local function get_combat_ban_states()
+ local str = file.Read("pac_combat_bans.txt", "DATA")
+
+ local banstates = {}
+
+ if str and str ~= "" then
+ banstates = util.KeyValuesToTable(str)
+ end
+
+ do -- check if this needs to be rebuilt
+ local k,v = next(banstates)
+ if isstring(v) then
+ local temp = {}
+
+ for k,v in pairs(banstates) do
+ permission = pac.global_combat_whitelist[player.GetBySteamID(k)] or "Default"
+ temp[util.CRC("gm_" .. v .. "_gm")] = {steamid = v, name = k, permission = permission}
+ end
+
+ banstates = temp
+ end
+ end
+
+ return banstates
+end
+
+local function load_table_from_file()
+ tbl_on_file = get_combat_ban_states()
+ for id, data in pairs(tbl_on_file) do
+ if not pac.global_combat_whitelist[id] then
+ pac.global_combat_whitelist[id] = tbl_on_file[id]
+ end
+ end
+end
+
+if SERVER then
+ util.AddNetworkString("pac.BanUpdate")
+ util.AddNetworkString("pac.RequestBanStates")
+ util.AddNetworkString("pac.SendBanStates")
+
+
+ util.AddNetworkString("pac.CombatBanUpdate")
+ util.AddNetworkString("pac.SendCombatBanStates")
+ util.AddNetworkString("pac.RequestCombatBanStates")
+end
+
+
+net.Receive("pac.CombatBanUpdate", function(len, player)
+ if not player:IsAdmin() then
+ return
+ end
+ --get old states first
+ pac.old_tbl_on_file = get_combat_ban_states()
+
+ load_table_from_file()
+
+ local combatstates_update = net.ReadTable()
+ local is_id_table = net.ReadBool()
+ local banstates_for_file = pac.old_tbl_on_file
+
+ --update
+ if not is_id_table then
+ for ply, perm in pairs(combatstates_update) do
+ banstates_for_file[ply:SteamID()] = {
+ steamid = ply:SteamID(),
+ nick = ply:Nick(),
+ permission = perm
+ }
+
+ pac.global_combat_whitelist[ply:SteamID()] = {
+ steamid = ply:SteamID(),
+ nick = ply:Nick(),
+ permission = perm
+ }
+ end
+ else
+ pac.global_combat_whitelist = combatstates_update
+ banstates_for_file = combatstates_update
+ end
+
+ file.Write("pac_combat_bans.txt", util.TableToKeyValues(banstates_for_file), "DATA")
+end)
+
+net.Receive("pac.RequestCombatBanStates", function(len, ply)
+ if not ply:IsAdmin() then
+ return
+ end
+ pac.global_combat_whitelist = get_combat_ban_states()
+ net.Start("pac.SendCombatBanStates")
+ net.WriteTable(pac.global_combat_whitelist)
+ net.Send(ply)
+end)
+
+
+pac.old_tbl_on_file = get_combat_ban_states()
+
+
+
+concommand.Add("pac_read_combat_bans", function()
+ print("PAC3 combat bans and whitelist:")
+ for k,v in pairs(get_combat_ban_states()) do
+ print("\t" .. v.nick .. " is " .. v.permission .. " [" .. v.steamid .. "]")
+ end
+end)
+
+concommand.Add("pac_read_outfit_bans", function()
+ PrintTable(pace.Bans)
+end)
diff --git a/lua/pac3/editor/server/init.lua b/lua/pac3/editor/server/init.lua
index b9c6eb142..32512077d 100644
--- a/lua/pac3/editor/server/init.lua
+++ b/lua/pac3/editor/server/init.lua
@@ -22,6 +22,8 @@ do
end
end
+CreateConVar("pac_sv_prop_outfits", "0", CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow applying parts on other entities serverside\n0=don't\n1=allow on props but not players\n2=allow on other players")
+
function pace.CanPlayerModify(ply, ent)
if not IsValid(ply) or not IsValid(ent) then
return false
@@ -43,6 +45,15 @@ function pace.CanPlayerModify(ply, ent)
return true
end
+ if GetConVar("pac_sv_prop_outfits"):GetInt() ~= 0 then
+ if GetConVar("pac_sv_prop_outfits"):GetInt() == 1 then
+ return not (ply ~= ent and ent:IsPlayer())
+ elseif GetConVar("pac_sv_prop_outfits"):GetInt() == 2 then
+ return true
+ end
+
+ end
+
do
local tr = util.TraceLine({ start = ply:EyePos(), endpos = ent:WorldSpaceCenter(), filter = ply })
if tr.Entity == ent and hook.Run("CanTool", ply, tr, "paint") == true then
@@ -50,6 +61,8 @@ function pace.CanPlayerModify(ply, ent)
end
end
+
+
return false
end
@@ -59,6 +72,7 @@ include("wear_filter.lua")
include("bans.lua")
include("spawnmenu.lua")
include("show_outfit_on_use.lua")
+include("pac_settings_manager.lua")
do
util.AddNetworkString("pac_in_editor")
@@ -92,4 +106,5 @@ end
CreateConVar("has_pac3_editor", "1", {FCVAR_NOTIFY})
+resource.AddSingleFile("materials/icon64/new pac icon.png")
resource.AddSingleFile("materials/icon64/pac3.png")
diff --git a/lua/pac3/editor/server/pac_settings_manager.lua b/lua/pac3/editor/server/pac_settings_manager.lua
new file mode 100644
index 000000000..900cd63bd
--- /dev/null
+++ b/lua/pac3/editor/server/pac_settings_manager.lua
@@ -0,0 +1,112 @@
+util.AddNetworkString("pac_send_sv_cvar")
+util.AddNetworkString("pac_request_sv_cvars")
+util.AddNetworkString("pac_send_cvars_to_client")
+
+--cvars used by settings.lua
+local pac_server_cvars = {
+ {"pac_sv_prop_protection", "Enforce generic prop protection for player-owned props and physics entities based on client consents.", "", -1, 0, 200},
+ {"pac_sv_combat_whitelisting", "Restrict new pac3 combat (damage zone, lock, force, hitscan, health modifier) to only whitelisted users.", "off = Blacklist mode: Default players are allowed to use the combat features\non = Whitelist mode: Default players aren't allowed to use the combat features until set to Allowed", -1, 0, 200},
+ {"pac_sv_block_combat_features_on_next_restart", "Block the combat features that aren't enabled. WARNING! Requires a restart!\nThis applies to damage zone, lock, force, hitscan and health modifier parts", "You can go to the console and set pac_sv_block_combat_features_on_next_restart to 2 to block everything.\nif you re-enable a blocked part, update with pac_sv_combat_reinitialize_missing_receivers", -1, 0, 200},
+ {"pac_sv_combat_enforce_netrate_monitor_serverside", "Enable serverside monitoring prints for allowance and rate limiters", "Enable serverside monitoring prints.\n0=let clients enforce their netrate allowance before sending messages\n1=the server will receive net messages and print the outcome.", -1, 0, 200},
+ {"pac_sv_combat_enforce_netrate", "Rate limiter (milliseconds)", "The milliseconds delay between net messages.\nIf this is 0, the allowance won't matter, otherwise early net messages use up the player's allowance.\nThe allowance regenerates gradually when unused, and one unit gets spent if the message is earlier than the rate limiter's delay.", 0, 0, 1000},
+ {"pac_sv_combat_enforce_netrate_buffersize", "Allowance, in number of messages", "Allowance:\nIf this is 0, only the time limiter will stop pac combat messages if they're too fast.\nOtherwise, players trying to use a pac combat message earlier will deduct 1 from the player's allowance, and only stop the messages if the allowance reaches 0.", 0, 0, 400},
+ {"pac_sv_entity_limit_per_combat_operation", "Hard entity limit to cutoff damage zones and force parts", "If the number of entities selected is more than this value, the whole operation gets dropped.\nThis is so that the server doesn't have to send huge amounts of entity updates to everyone.", 0, 0, 1000},
+ {"pac_sv_entity_limit_per_player_per_combat_operation", "Entity limit per player to cutoff damage zones and force parts", "When in multiplayer, with the server's player count, if the number of entities selected is more than this value, the whole operation gets dropped.\nThis is so that the server doesn't have to send huge amounts of entity updates to everyone.", 0, 0, 500},
+ {"pac_sv_player_limit_as_fraction_to_drop_damage_zone", "block damage zones targeting this fraction of players", "This applies when the zone covers more than 12 players. 0 is 0% of the server, 1 is 100%\nFor example, if this is at 0.5, there are 24 players and a damage zone covers 13 players, it will be blocked.", 2, 0, 1},
+ {"pac_sv_combat_distance_enforced", "distance to block combat actions that are too far", "The distance is compared between the action's origin and the player's position.\n0 to ignore.", 0, 0, 64000},
+
+ {"pac_sv_lock", "Allow lock part", "", -1, 0, 200},
+ {"pac_sv_lock_teleport", "Allow lock part teleportation", "", -1, 0, 200},
+ {"pac_sv_lock_grab", "Allow lock part grabbing", "", -1, 0, 200},
+ {"pac_sv_lock_aim", "Allow lock part aiming", "", -1, 0, 200},
+ {"pac_sv_lock_allow_grab_ply", "Allow grabbing players", "", -1, 0, 200},
+ {"pac_sv_lock_allow_grab_npc", "Allow grabbing NPCs", "", -1, 0, 200},
+ {"pac_sv_lock_allow_grab_ent", "Allow grabbing other entities", "", -1, 0, 200},
+ {"pac_sv_lock_max_grab_radius", "Max lock part grab range", "", 0, 0, 5000},
+
+ {"pac_sv_damage_zone", "Allow damage zone", "", -1, 0, 200},
+ {"pac_sv_damage_zone_max_radius", "Max damage zone radius", "", 0, 0, 32767},
+ {"pac_sv_damage_zone_max_length", "Max damage zone length", "", 0, 0, 32767},
+ {"pac_sv_damage_zone_max_damage", "Max damage zone damage", "", 0, 0, 268435455},
+ {"pac_sv_damage_zone_allow_dissolve", "Allow damage entity dissolvers", "", -1, 0, 200},
+ {"pac_sv_damage_zone_allow_ragdoll_hitparts", "Allow ragdoll hitparts", "", -1, 0, 200},
+
+ {"pac_sv_force", "Allow force part", "", -1, 0, 200},
+ {"pac_sv_force_max_radius", "Max force radius", "", 0, 0, 32767},
+ {"pac_sv_force_max_length", "Max force length", "", 0, 0, 32767},
+ {"pac_sv_force_max_amount", "Max force amount", "", 0, 0, 10000000},
+
+ {"pac_sv_hitscan", "allow serverside bullets", "", -1, 0, 200},
+ {"pac_sv_hitscan_max_damage", "Max hitscan damage (per bullet, per multishot,\ndepending on the next setting)", "", 0, 0, 268435455},
+ {"pac_sv_hitscan_divide_max_damage_by_max_bullets", "force hitscans to distribute their total damage accross bullets. if off, every bullet does full damage; if on, adding more bullets doesn't do more damage", "", -1, 0, 200},
+ {"pac_sv_hitscan_max_bullets", "Maximum number of bullets for hitscan multishots", "", 0, 0, 500},
+
+ {"pac_sv_projectiles", "allow serverside physical projectiles", "", -1, 0, 200},
+ {"pac_sv_projectile_allow_custom_collision_mesh", "allow custom collision meshes for physical projectiles", "", -1, 0, 200},
+ {"pac_sv_projectile_max_phys_radius", "Max projectile physical radius", "", 0, 0, 4095},
+ {"pac_sv_projectile_max_damage_radius", "Max projectile damage radius", "", 0, 0, 4095},
+ {"pac_sv_projectile_max_attract_radius", "Max projectile attract radius", "", 0, 0, 100000000},
+ {"pac_sv_projectile_max_damage", "Max projectile damage", "", 0, 0, 100000000},
+ {"pac_sv_projectile_max_speed", "Max projectile speed", "", 0, 0, 50000},
+ {"pac_sv_projectile_max_mass", "Max projectile mass", "", 0, 0, 500000},
+
+ {"pac_sv_health_modifier", "Allow health modifier part", "", -1, 0, 200},
+ {"pac_sv_health_modifier_allow_maxhp", "Allow changing max health and max armor", "", -1, 0, 200},
+ {"pac_sv_health_modifier_max_hp_armor", "Maximum value for max health / armor modification", "", 0, 0, 100000000},
+ {"pac_sv_health_modifier_min_damagescaling", "Minimum combined damage multiplier allowed.\nNegative values lead to healing from damage.", "", 2, -10, 1},
+ {"pac_sv_health_modifier_extra_bars", "Allow extra healthbars", "What are those? It's like an armor layer that takes damage before it gets applied to the entity.", -1, 0, 200},
+ {"pac_sv_health_modifier_allow_counted_hits", "Allow extra healthbars counted hits mode", "1 EX HP absorbs 1 whole hit.", -1, 0, 200},
+ {"pac_sv_health_modifier_max_extra_bars_value", "Maximum combined value for extra healthbars", "", 0, 0, 100000000},
+
+
+ {"pac_modifier_blood_color", "Blood", "", -1, 0, 200},
+ {"pac_allow_mdl", "MDL", "", -1, 0, 200},
+ {"pac_allow_mdl_entity", "Entity MDL", "", -1, 0, 200},
+ {"pac_modifier_model", "Entity model", "", -1, 0, 200},
+ {"pac_modifier_size", "Entity size", "", -1, 0, 200},
+
+ --the playermovement enabler policy cvar is a form, not a slider nor a bool
+ {"pac_player_movement_allow_mass", "Allow Modify Mass", "", -1, 0, 200},
+ {"pac_player_movement_min_mass", "Mimnimum mass players can set for themselves", "", 0, 0, 1000000},
+ {"pac_player_movement_max_mass", "Maximum mass players can set for themselves", "", 0, 0, 1000000},
+ {"pac_player_movement_physics_damage_scaling", "Allow damage scaling of physics damage based on player's mass", "", -1, 0, 200},
+
+ {"pac_sv_draw_distance", "PAC server draw distance", "", 0, 0, 500000},
+ {"pac_submit_spam", "Limit pac_submit to prevent spam", "", -1, 0, 200},
+ {"pac_submit_limit", "limit of pac_submits", "", 0, 0, 100},
+ {"pac_onuse_only_force", "Players need to +USE on others to reveal outfits", "", -1, 0, 200},
+ {"pac_sv_prop_outfits", "allow prop / other player outfits", "0 = don't allow\n1 = allow applying outfits on props/npcs\n2 = allow applying outfits on other players", 0, 0, 2},
+
+ {"sv_pac_webcontent_allow_no_content_length", "Players need to +USE on others to reveal outfits", "", -1, 0, 200},
+ {"pac_to_contraption_allow", "Allow PAC to contraption tool", "", -1, 0, 200},
+ {"pac_max_contraption_entities", "Entity limit for PAC to contraption", "", 0, 0, 200},
+ {"pac_restrictions", "restrict PAC editor camera movement", "", -1, 0, 200},
+}
+
+net.Receive("pac_send_sv_cvar", function(len,ply)
+ if not (game.SinglePlayer() or ply:IsAdmin()) then ply:ChatPrint( "Only admins can change pac3 server settings!" ) return end
+ local cmd = net.ReadString()
+ local val = net.ReadString()
+ if not cmd then return end
+
+ if GetConVar(cmd) then
+ GetConVar(cmd):SetString(val)
+ end
+
+end)
+
+net.Receive("pac_request_sv_cvars", function (len, ply)
+ local cvars_tbl = {}
+ for _, tbl in ipairs(pac_server_cvars) do
+ local cmd = tbl[1]
+ if GetConVar(cmd) then
+ cvars_tbl[cmd] = GetConVar(cmd):GetString()
+ end
+ end
+ timer.Simple(0, function()
+ net.Start("pac_send_cvars_to_client")
+ net.WriteTable(cvars_tbl)
+ net.Send(ply)
+ end)
+
+end)
diff --git a/lua/pac3/editor/server/util.lua b/lua/pac3/editor/server/util.lua
index c59e607c2..d1a608070 100644
--- a/lua/pac3/editor/server/util.lua
+++ b/lua/pac3/editor/server/util.lua
@@ -15,7 +15,7 @@ end
function pace.CallHook(str, ...)
- return hook.Call("pac_" .. str, GAMEMODE, ...)
+ return hook.Call("pace_" .. str, GAMEMODE, ...)
end
diff --git a/lua/pac3/editor/server/wear.lua b/lua/pac3/editor/server/wear.lua
index f4472fb69..6a35acfe7 100644
--- a/lua/pac3/editor/server/wear.lua
+++ b/lua/pac3/editor/server/wear.lua
@@ -7,6 +7,7 @@ local isfunction = isfunction
local ProtectedCall = ProtectedCall
pace.StreamQueue = pace.StreamQueue or {}
+pace.MaxStreamQueue = 32 -- Max queued outfits per player
timer.Create("pac_check_stream_queue", 0.1, 0, function()
local item = table.remove(pace.StreamQueue)
@@ -46,17 +47,10 @@ local function make_copy(tbl, input)
end
local function net_write_table(tbl)
-
local buffer = pac.StringStream()
buffer:writeTable(tbl)
-
local data = buffer:getString()
- local ok, err = pcall(net.WriteStream, data)
-
- if not ok then
- return ok, err
- end
-
+ net.WriteStream(data)
return #data
end
@@ -269,20 +263,22 @@ function pace.SubmitPartNow(data, filter)
if not players or istable(players) and not next(players) then return true end
-- Alternative transmission system
- local ret = hook.Run("pac_SendData", players, data)
+ local ret = pac.CallHook("SendData", players, data)
if ret == nil then
net.Start("pac_submit")
- local bytes, err = net_write_table(data)
+ net.WriteEntity(owner)
+ local ok, err = pcall(net_write_table, data)
- if not bytes then
+ if ok then
+ net.Send(players)
+ else
+ net.Abort()
local errStr = tostring(err)
- ErrorNoHalt("[PAC3] Outfit broadcast failed for " .. tostring(owner) .. ": " .. errStr .. '\n')
+ ErrorNoHalt("[PAC3] Outfit broadcast failed for " .. tostring(owner) .. ": " .. errStr .. "\n")
if owner and owner:IsValid() then
- owner:ChatPrint('[PAC3] ERROR: Could not broadcast your outfit: ' .. errStr)
+ owner:ChatPrint("[PAC3] ERROR: Could not broadcast your outfit: " .. errStr)
end
- else
- net.Send(players)
end
end
@@ -299,18 +295,22 @@ end
-- Inserts the given part into the StreamQueue
function pace.SubmitPart(data, filter, callback)
- if istable(data.part) then
- pac.dprint("queuing part %q from %s", data.part.self.Name, tostring(data.owner))
- table.insert(pace.StreamQueue, {
- data = data,
- filter = filter,
- callback = callback
- })
-
- return "queue"
+ if not ((istable(data.part) or isstring(data.part)) and IsValid(data.owner)) then return end
+ local owner = data.owner
+ local count = 0
+ for _, v in ipairs(pace.StreamQueue) do
+ if v.data.owner == owner then
+ if count == pace.MaxStreamQueue then return end
+ count = count + 1
+ end
end
- return pace.SubmitPartNow(data, filter)
+ if data.part.self then pac.dprint("queuing part %q from %s", data.part.self.Name, tostring(data.owner)) end
+ table.insert(pace.StreamQueue, {
+ data = data,
+ filter = filter,
+ callback = callback
+ })
end
-- Inserts the given part into the StreamQueue, and notifies when it completes
@@ -378,8 +378,8 @@ end
util.AddNetworkString("pac_submit")
-local pac_submit_spam = CreateConVar('pac_submit_spam', '1', {FCVAR_NOTIFY, FCVAR_ARCHIVE}, 'Prevent users from spamming pac_submit')
-local pac_submit_limit = CreateConVar('pac_submit_limit', '30', {FCVAR_NOTIFY, FCVAR_ARCHIVE}, 'pac_submit spam limit')
+local pac_submit_spam = CreateConVar("pac_submit_spam", "1", {FCVAR_NOTIFY, FCVAR_ARCHIVE}, "Prevent users from spamming pac_submit")
+local pac_submit_limit = CreateConVar("pac_submit_limit", "30", {FCVAR_NOTIFY, FCVAR_ARCHIVE}, "pac_submit spam limit")
pace.PCallNetReceive(net.Receive, "pac_submit", function(len, ply)
if len < 64 then return end
@@ -402,15 +402,15 @@ pace.PCallNetReceive(net.Receive, "pac_submit", function(len, ply)
return
end
local buffer = pac.StringStream(data)
- pace.HandleReceivedData(ply, buffer:readTable())
+ local ok,tbl = pcall(buffer.readTable, buffer)
+ if ok then
+ pace.HandleReceivedData(ply, tbl)
+ end
end)
end)
function pace.ClearOutfit(ply)
- local uid = pac.Hash(ply)
-
- pace.SubmitPart({part = "__ALL__", uid = pac.Hash(ply), owner = ply})
- pace.CallHook("RemoveOutfit", ply)
+ pace.RemovePart({part = "__ALL__", uid = pac.Hash(ply), owner = ply})
end
function pace.RequestOutfits(ply)
diff --git a/lua/pac3/extra/client/wire_expression_extension.lua b/lua/pac3/extra/client/wire_expression_extension.lua
index f3e162b97..5f19e1e72 100644
--- a/lua/pac3/extra/client/wire_expression_extension.lua
+++ b/lua/pac3/extra/client/wire_expression_extension.lua
@@ -33,14 +33,24 @@ local function SetKeyValue(ply, ent, unique_id, key, val)
end
end
-net.Receive("pac_e2_setkeyvalue_str", function()
+net.Receive("pac_e2_setkeyvalue", function()
local ply = net.ReadEntity()
if ply:IsValid() then
local ent = net.ReadEntity()
local id = net.ReadString()
local key = net.ReadString()
- local val = net.ReadString()
+ local type = net.ReadUInt(2) ---@type pac.E2.NetID
+ local val
+ if type == 0 then
+ val = net.ReadString()
+ elseif type == 1 then
+ val = net.ReadFloat()
+ elseif type == 2 then
+ val = net.ReadVector()
+ else
+ val = net.ReadAngle()
+ end
SetKeyValue(ply, ent, id, key, val)
end
diff --git a/lua/pac3/extra/shared/init.lua b/lua/pac3/extra/shared/init.lua
index 78de1c3e9..5381081a1 100644
--- a/lua/pac3/extra/shared/init.lua
+++ b/lua/pac3/extra/shared/init.lua
@@ -2,11 +2,12 @@
include("hands.lua")
include("pac_weapon.lua")
include("projectiles.lua")
+include("net_combat.lua")
local cvar = CreateConVar("pac_restrictions", "0", FCVAR_REPLICATED)
if CLIENT then
- pac.AddHook("pac_EditorCalcView", "pac_restrictions", function()
+ pac.AddHook("pac_EditorCalcView", "restrictions", function()
if cvar:GetInt() > 0 and not pac.LocalPlayer:IsAdmin() then
local ent = pace.GetViewEntity()
local dir = pace.ViewPos - ent:EyePos()
diff --git a/lua/pac3/extra/shared/net_combat.lua b/lua/pac3/extra/shared/net_combat.lua
new file mode 100644
index 000000000..663e7e0f3
--- /dev/null
+++ b/lua/pac3/extra/shared/net_combat.lua
@@ -0,0 +1,2906 @@
+--lua_openscript pac3/extra/shared/net_combat.lua
+if SERVER then
+ include("pac3/editor/server/combat_bans.lua")
+ include("pac3/editor/server/bans.lua")
+end
+
+local master_default = "0"
+
+if string.find(engine.ActiveGamemode(), "sandbox") and game.SinglePlayer() then
+ master_default = "1"
+end
+
+pac.global_combat_whitelist = pac.global_combat_whitelist or {}
+
+local hitscan_allow = CreateConVar("pac_sv_hitscan", master_default, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow hitscan parts serverside")
+local hitscan_max_bullets = CreateConVar("pac_sv_hitscan_max_bullets", "200", CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "hitscan part maximum number of bullets")
+local hitscan_max_damage = CreateConVar("pac_sv_hitscan_max_damage", "20000", CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "hitscan part maximum damage")
+local hitscan_spreadout_dmg = CreateConVar("pac_sv_hitscan_divide_max_damage_by_max_bullets", 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Whether or not force hitscans to divide their damage among the number of bullets fired")
+
+local damagezone_allow = CreateConVar("pac_sv_damage_zone", master_default, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow damage zone parts serverside")
+local damagezone_max_damage = CreateConVar("pac_sv_damage_zone_max_damage", "20000", CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "damage zone maximum damage")
+local damagezone_max_length = CreateConVar("pac_sv_damage_zone_max_length", "20000", CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "damage zone maximum length")
+local damagezone_max_radius = CreateConVar("pac_sv_damage_zone_max_radius", "10000", CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "damage zone maximum radius")
+local damagezone_allow_dissolve = CreateConVar("pac_sv_damage_zone_allow_dissolve", "1", CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Whether to enable entity dissolvers and removing NPCs\" weapons on death for damagezone")
+local damagezone_allow_damageovertime = CreateConVar("pac_sv_damage_zone_allow_damage_over_time", "1", CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow damage over time for damagezone")
+local damagezone_max_damageovertime_total_time = CreateConVar("pac_sv_damage_zone_max_damage_over_time_total_time", "1", CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "maximum time that a DoT instance is allowed to last in total.\nIf your tick time multiplied by the count is beyond that, it will compress the ticks, but if your total time is more than 200% of the limit, it will reject the attack")
+local damagezone_allow_ragdoll_networking_for_hitpart = CreateConVar("pac_sv_damage_zone_allow_ragdoll_hitparts", "0", CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Whether to send information about corpses to all clients when a player's damage zone needs it for attaching hitparts")
+
+local lock_allow = CreateConVar("pac_sv_lock", master_default, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow lock parts serverside")
+local lock_allow_grab = CreateConVar("pac_sv_lock_grab", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow lock part grabs serverside")
+local lock_allow_teleport = CreateConVar("pac_sv_lock_teleport", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow lock part teleports serverside")
+local lock_allow_aim = CreateConVar("pac_sv_lock_aim", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow lock part aim serverside")
+local lock_max_radius = CreateConVar("pac_sv_lock_max_grab_radius", "200", CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "lock part maximum grab radius")
+local lock_allow_grab_ply = CreateConVar("pac_sv_lock_allow_grab_ply", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "allow grabbing players with lock part")
+local lock_allow_grab_npc = CreateConVar("pac_sv_lock_allow_grab_npc", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "allow grabbing NPCs with lock part")
+local lock_allow_grab_ent = CreateConVar("pac_sv_lock_allow_grab_ent", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "allow grabbing other entities with lock part")
+
+local force_allow = CreateConVar("pac_sv_force", master_default, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow force parts serverside")
+local force_max_length = CreateConVar("pac_sv_force_max_length", "10000", CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "force part maximum length")
+local force_max_radius = CreateConVar("pac_sv_force_max_radius", "10000", CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "force part maximum radius")
+local force_max_amount = CreateConVar("pac_sv_force_max_amount", "10000", CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "force part maximum amount of force")
+
+local healthmod_allow = CreateConVar("pac_sv_health_modifier", master_default, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow health modifier parts serverside")
+local healthmod_allowed_extra_bars = CreateConVar("pac_sv_health_modifier_extra_bars", 1, CLIENT and {FCVAR_NOTIFY, FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow extra health bars")
+local healthmod_allow_change_maxhp = CreateConVar("pac_sv_health_modifier_allow_maxhp", 1, CLIENT and {FCVAR_NOTIFY, FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow players to change their maximum health and armor.")
+local healthmod_minimum_dmgscaling = CreateConVar("pac_sv_health_modifier_min_damagescaling", -1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Minimum health modifier amount. Negative values can heal.")
+local healthmod_allowed_counted_hits = CreateConVar("pac_sv_health_modifier_allow_counted_hits", 1, CLIENT and {FCVAR_NOTIFY, FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Allow extra health bars counted hits mode (one hit = 1 HP)")
+local healthmod_max_value = CreateConVar("pac_sv_health_modifier_max_hp_armor", 1000000, CLIENT and {FCVAR_NOTIFY, FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "health modifier maximum value for health and armor")
+local healthmod_max_extra_bars_value = CreateConVar("pac_sv_health_modifier_max_extra_bars_value", 1000000, CLIENT and {FCVAR_NOTIFY, FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "health modifier maximum value for extra health bars (bars x amount)")
+
+local master_init_featureblocker = CreateConVar("pac_sv_block_combat_features_on_next_restart", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Whether to stop initializing the net receivers for the networking of PAC3 combat parts those selectively disabled. This requires a restart!\n0=initialize all the receivers\n1=disable those whose corresponding part cvar is disabled\n2=block all combat features\nAfter updating the sv cvars, you can still reinitialize the net receivers with pac_sv_combat_reinitialize_missing_receivers, but you cannot turn them off after they are turned on")
+cvars.AddChangeCallback("pac_sv_block_combat_features_on_next_restart", function() print("Remember that pac_sv_block_combat_features_on_next_restart is applied on server startup! Only do it if you know what you're doing. You'll need to restart the server.") end)
+
+local debugging = CreateConVar("pac_sv_combat_debugging", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Whether to get log prints for combat activity. If a player targets too many entities or sends messages too often, it will say it in the server console.")
+local enforce_netrate = CreateConVar("pac_sv_combat_enforce_netrate", 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "whether to enforce a limit on how often any pac combat net messages can be sent. 0 to disable, otherwise a number in mililiseconds.\nSee the related cvar pac_sv_combat_enforce_netrate_buffersize. That second convar is governed by this one, if the netrate enforcement is 0, the allowance doesn\"t matter")
+local netrate_allowance = CreateConVar("pac_sv_combat_enforce_netrate_buffersize", 60, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "the budgeted allowance to limit how many pac combat net messages can be sent in bursts. 0 to disable, otherwise a number of net messages of allowance.")
+local netrate_enforcement_sv_monitoring = CreateConVar("pac_sv_combat_enforce_netrate_monitor_serverside", 0, {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Whether or not to let clients enforce their net message rates.\nSet this to 1 to get serverside prints telling you whenever someone is going over their allowance, but it'll still take the network bandwidth.\nSet this to 0 to let clients enforce their net rate and save some bandwidth but the server won't know who's spamming net messages.")
+local raw_ent_limit = CreateConVar("pac_sv_entity_limit_per_combat_operation", 500, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Hard limit to drop any force or damage zone if more than this amount of entities is selected")
+local per_ply_limit = CreateConVar("pac_sv_entity_limit_per_player_per_combat_operation", 40, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Limit per player to drop any force or damage zone if this amount multiplied by each client is more than the hard limit")
+local player_fraction = CreateConVar("pac_sv_player_limit_as_fraction_to_drop_damage_zone", 1, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "The fraction (0.0-1.0) of players that will stop damage zone net messages if a damage zone order covers more than this fraction of the server's population, when there are more than 12 players covered")
+local enforce_distance = CreateConVar("pac_sv_combat_distance_enforced", 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Whether to enforce a limit on how far a pac combat action can originate.\nIf set to a distance, it will prevent actions that are too far from the acting player.\n0 to disable.")
+local ENFORCE_DISTANCE_SQR = math.pow(enforce_distance:GetInt(),2)
+cvars.AddChangeCallback("pac_sv_combat_distance_enforced", function() ENFORCE_DISTANCE_SQR = math.pow(enforce_distance:GetInt(),2) end)
+
+
+local global_combat_whitelisting = CreateConVar("pac_sv_combat_whitelisting", 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "How the server should decide which players are allowed to use the main PAC3 combat parts (lock, damagezone, force...).\n0:Everyone is allowed unless the parts are disabled serverwide\n1:No one is allowed until they get verified as trustworthy\tpac_sv_whitelist_combat \n\tpac_sv_blacklist_combat ")
+local global_combat_prop_protection = CreateConVar("pac_sv_prop_protection", 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Whether players owned (created) entities (physics props and gmod contraption entities) will be considered in the consent calculations, protecting them. Without this cvar, only the player is protected.")
+
+do --define a basic class for the bullet emitters
+ local ENT = {}
+ ENT.Type = "anim"
+ ENT.ClassName = "pac_bullet_emitter"
+ ENT.Spawnable = false
+ scripted_ents.Register(ENT, "pac_bullet_emitter")
+end
+
+if SERVER then
+ local damageable_point_ent_classes = {
+ ["predicted_viewmodel"] = false,
+ ["prop_physics"] = true,
+ ["weapon_striderbuster"] = true,
+ ["item_item_crate"] = true,
+ ["npc_satchel"] = true,
+ ["func_breakable_surf"] = true,
+ ["func_breakable"] = true,
+ ["func_physbox"] = true,
+ ["physics_cannister"] = true
+ }
+
+ local physics_point_ent_classes = {
+ ["prop_physics"] = true,
+ ["prop_physics_multiplayer"] = true,
+ ["prop_ragdoll"] = true,
+ ["weapon_striderbuster"] = true,
+ ["item_item_crate"] = true,
+ ["func_breakable_surf"] = true,
+ ["func_breakable"] = true,
+ ["physics_cannister"] = true
+ }
+
+ local contraption_classes = {
+ ["prop_physics"] = true,
+ }
+
+ local pre_excluded_ent_classes = {
+ ["info_player_start"] = true,
+ ["aoc_spawnpoint"] = true,
+ ["info_player_teamspawn"] = true,
+ ["env_tonemap_controller"] = true,
+ ["env_fog_controller"] = true,
+ ["env_skypaint"] = true,
+ ["shadow_control"] = true,
+ ["env_sun"] = true,
+ ["predicted_viewmodel"] = true,
+ ["physgun_beam"] = true,
+ ["ambient_generic"] = true,
+ ["trigger_once"] = true,
+ ["trigger_multiple"] = true,
+ ["trigger_hurt"] = true,
+ ["info_ladder_dismount"] = true,
+ ["info_particle_system"] = true,
+ ["env_sprite"] = true,
+ ["env_fire"] = true,
+ ["env_soundscape"] = true,
+ ["env_smokestack"] = true,
+ ["light"] = true,
+ ["move_rope"] = true,
+ ["keyframe_rope"] = true,
+ ["env_soundscape_proxy"] = true,
+ ["gmod_hands"] = true,
+ ["env_lightglow"] = true,
+ ["point_spotlight"] = true,
+ ["spotlight_end"] = true,
+ ["beam"] = true,
+ ["info_target"] = true,
+ ["func_lod"] = true,
+
+ }
+
+
+ local grab_consents = {}
+ local damage_zone_consents = {}
+ local force_consents = {}
+ local hitscan_consents = {}
+ local calcview_consents = {}
+ local active_force_ids = {}
+ local active_grabbed_ents = {}
+
+
+ local friendly_NPC_preferences = {}
+ --we compare player's preference with the disposition's overall "friendliness". if relationship is more friendly than the preference, do not affect
+ local disposition_friendliness_level = {
+ [0] = 0, --D_ER Error
+ [1] = 0, --D_HT Hate
+ [2] = 1, --D_FR Frightened / Fear
+ [3] = 2, --D_LI Like
+ [4] = 1, --D_NU Neutral
+ }
+
+ local function Is_NPC(ent)
+ return ent:IsNPC() or ent:IsNextBot() or ent.IsDrGEntity or ent.IsVJBaseSNPC
+ end
+
+ local function NPCDispositionAllowsIt(ply, ent)
+
+ if not Is_NPC(ent) or not ent.Disposition then return true end
+
+ if not friendly_NPC_preferences[ply] then return true end
+
+ local player_friendliness = friendly_NPC_preferences[ply]
+ local relationship_friendliness = disposition_friendliness_level[ent:Disposition(ply)]
+
+ if player_friendliness == 0 then --me agressive
+ return true --hurt anyone
+ elseif player_friendliness == 1 then --me not fully agressive
+ return relationship_friendliness <= 1 --hurt who is neutral or hostile
+ elseif player_friendliness == 2 then --me mostly friendly
+ return relationship_friendliness == 0 --hurt who is hostile
+ end
+
+ return true
+ end
+
+ local function NPCDispositionIsFilteredOut(ply, ent, friendly, neutral, hostile)
+ if not Is_NPC(ent) or not ent.Disposition then return false end
+ local relationship_friendliness = disposition_friendliness_level[ent:Disposition(ply)]
+
+ if relationship_friendliness == 0 then --it hostile
+ return not hostile
+ elseif relationship_friendliness == 1 then --it neutral
+ return not neutral
+ elseif relationship_friendliness == 2 then --it friendly
+ return not friendly
+ end
+ end
+
+ local damage_types = {
+ generic = 0, --generic damage
+ crush = 1, --caused by physics interaction
+ bullet = 2, --bullet damage
+ slash = 4, --sharp objects, such as manhacks or other npcs attacks
+ burn = 8, --damage from fire
+ vehicle = 16, --hit by a vehicle
+ fall = 32, --fall damage
+ blast = 64, --explosion damage
+ club = 128, --crowbar damage
+ shock = 256, --electrical damage, shows smoke at the damage position
+ sonic = 512, --sonic damage,used by the gargantua and houndeye npcs
+ energybeam = 1024, --laser
+ nevergib = 4096, --don't create gibs
+ alwaysgib = 8192, --always create gibs
+ drown = 16384, --drown damage
+ paralyze = 32768, --same as dmg_poison
+ nervegas = 65536, --neurotoxin damage
+ poison = 131072, --poison damage
+ acid = 1048576, --
+ airboat = 33554432, --airboat gun damage
+ blast_surface = 134217728, --this won't hurt the player underwater
+ buckshot = 536870912, --the pellets fired from a shotgun
+ direct = 268435456, --
+ dissolve = 67108864, --forces the entity to dissolve on death
+ drownrecover = 524288, --damage applied to the player to restore health after drowning
+ physgun = 8388608, --damage done by the gravity gun
+ plasma = 16777216, --
+ prevent_physics_force = 2048, --
+ radiation = 262144, --radiation
+ removenoragdoll = 4194304, --don't create a ragdoll on death
+ slowburn = 2097152, --
+
+ fire = -1, -- ent:Ignite(5)
+
+ -- env_entity_dissolver
+ dissolve_energy = 0,
+ dissolve_heavy_electrical = 1,
+ dissolve_light_electrical = 2,
+ dissolve_core_effect = 3,
+
+ heal = -1,
+ armor = -1,
+ }
+ local special_damagetypes = {
+ fire = true, -- ent:Ignite(5)
+ -- env_entity_dissolver
+ dissolve_energy = 0,
+ dissolve_heavy_electrical = 1,
+ dissolve_light_electrical = 2,
+ dissolve_core_effect = 3,
+
+ heal = true,
+ armor = true,
+ }
+
+ local when_to_print_messages = {}
+ local can_print = {}
+ local function CountDebugMessage(ply)
+ if CurTime() < when_to_print_messages[ply] then
+ can_print[ply] = false
+ else
+ can_print[ply] = true
+ end
+ when_to_print_messages[ply] = CurTime() + 1
+ end
+ local function CountNetMessage(ply)
+ if can_print[ply] == nil then can_print[ply] = true end
+ when_to_print_messages[ply] = when_to_print_messages[ply] or 0
+
+ local stime = SysTime()
+ local ms_basis = enforce_netrate:GetInt()/1000
+ local base_allowance = netrate_allowance:GetInt()
+
+ ply.pac_netmessage_allowance = ply.pac_netmessage_allowance or base_allowance
+ ply.pac_netmessage_allowance_time = ply.pac_netmessage_allowance_time or 0 --initialize fields
+
+ local timedelta = stime - ply.pac_netmessage_allowance_time --in seconds
+ ply.pac_netmessage_allowance_time = stime
+ local regen_rate = math.Clamp(ms_basis,0.01,10) / 20 --delay (converted from milliseconds) -> frequency (1/seconds)
+ local regens = timedelta / regen_rate
+ --print(timedelta .. " s, " .. 1/regen_rate .. "/s, " .. regens .. " regens")
+ if base_allowance == 0 then --limiting only by time, with no reserves
+ return timedelta > ms_basis
+ elseif ms_basis == 0 then --allowance with 0 time means ??? I guess automatic pass
+ return true
+ else
+ if timedelta > ms_basis then --good, count up
+ --print("good time: +"..regens .. "->" .. math.Clamp(ply.pac_netmessage_allowance + math.min(regens,base_allowance), -1, base_allowance))
+ ply.pac_netmessage_allowance = math.Clamp(ply.pac_netmessage_allowance + math.min(regens,base_allowance), -1, base_allowance)
+ else --earlier than base delay, so count down the allowance
+ --print("bad time: -1")
+ ply.pac_netmessage_allowance = ply.pac_netmessage_allowance - 1
+ end
+ ply.pac_netmessage_allowance = math.Clamp(ply.pac_netmessage_allowance,-1,base_allowance)
+ ply.pac_netmessage_allowance_time = stime
+ return ply.pac_netmessage_allowance ~= -1
+ end
+
+ end
+
+ local function SetNoCPPIFallbackOwner(ent, ply)
+ ent.pac_prop_protection_owner = ply
+ end
+
+ local function Try_CPPIGetOwner(ent)
+ if ent.CPPIGetOwner then --a prop protection using CPPI probably exists so we use it
+ return ent:CPPIGetOwner()
+ end
+ return ent.pac_prop_protection_owner or nil --otherwise we'll use the field we set or
+ end
+
+ --hack fix to stop GetOwner returning [NULL Entity]
+ --uses CPPI interface from prop protectors if present
+ hook.Add("PlayerSpawnedProp", "HackReAssignOwner", function(ply, model, ent)
+ SetNoCPPIFallbackOwner(ent, ply)
+ end)
+ hook.Add("PlayerSpawnedNPC", "PAC_HackReAssignOwner", function(ply, ent)
+ SetNoCPPIFallbackOwner(ent, ply)
+ end)
+ hook.Add("PlayerSpawnedRagdoll", "PAC_HackReAssignOwner", function(ply, model, ent)
+ SetNoCPPIFallbackOwner(ent, ply)
+ end)
+ hook.Add("PlayerSpawnedSENT", "PAC_HackReAssignOwner", function(ply, ent)
+ SetNoCPPIFallbackOwner(ent, ply)
+ end)
+ hook.Add("PlayerSpawnedSWEP", "PAC_HackReAssignOwner", function(ply, ent)
+ SetNoCPPIFallbackOwner(ent, ply)
+ end)
+ hook.Add("PlayerSpawnedVehicle", "PAC_HackReAssignOwner", function(ply, ent)
+ SetNoCPPIFallbackOwner(ent, ply)
+ end)
+ hook.Add("PlayerSpawnedEffect", "PAC_HackReAssignOwner", function(ply, model, ent)
+ SetNoCPPIFallbackOwner(ent, ply)
+ end)
+
+ local function IsPossibleContraptionEntity(ent)
+ if not IsValid(ent) then return false end
+ local b = (string.find(ent:GetClass(), "phys") ~= nil
+ or string.find(ent:GetClass(), "gmod") ~= nil
+ or ent:IsConstraint())
+ return b
+ end
+
+ local function IsPropProtected(ent, ply)
+ local owner = Try_CPPIGetOwner(ent)
+
+ local prop_protected
+ if IsValid(owner) then --created entities should be fine
+ prop_protected = owner:IsPlayer() and owner ~= ply
+ else --players and world props could nil out
+ prop_protected = false
+ end
+
+ local reason = ""
+ local pac_sv_prop_protection = global_combat_prop_protection:GetBool()
+
+ local contraption = IsPossibleContraptionEntity(ent) and ent:IsConstrained()
+
+ if prop_protected and contraption then
+ reason = "it's a contraption owned by another player"
+ return true, reason
+ end
+ --apply prop protection
+ if pac_sv_prop_protection and prop_protected then
+ reason = "we enforce generic prop protection in the server"
+ return true, reason
+ end
+ return false, "it's fine"
+ end
+
+ --whitelisting/blacklisting check
+ local function PlayerIsCombatAllowed(ply)
+ if pace.IsBanned(ply) then return false end
+ if ulx and (ply.frozen or ply.jail) then return false end
+ if pac.global_combat_whitelist[string.lower(ply:SteamID())] then
+ if pac.global_combat_whitelist[string.lower(ply:SteamID())].permission == "Allowed" then return true end
+ if pac.global_combat_whitelist[string.lower(ply:SteamID())].permission == "Banned" then return false end
+ end
+
+ if global_combat_whitelisting:GetBool() then --if server uses the high-trust whitelisting mode
+ if pac.global_combat_whitelist[string.lower(ply:SteamID())] then
+ if pac.global_combat_whitelist[string.lower(ply:SteamID())].permission ~= "Allowed" then return false end --if player is not in whitelist, stop!
+ end
+ else --if server uses the default, blacklisting mode
+ if pac.global_combat_whitelist[string.lower(ply:SteamID())] then
+ if pac.global_combat_whitelist[string.lower(ply:SteamID())].permission == "Banned" then return false end --if player is in blacklist, stop!
+ end
+ end
+
+ return true
+ end
+
+ --stopping condition to stop force or damage operation if too many entities, because net impact is proportional to players
+ local function TooManyEnts(count, ply)
+ local playercount = player.GetCount()
+ local hard_limit = raw_ent_limit:GetInt()
+ local per_ply = per_ply_limit:GetInt()
+ --print(count .. " compared against hard limit " .. hard_limit .. " and " .. playercount .. " players*" .. per_ply .. " limit (" .. count*playercount .. " | " .. playercount*per_ply .. ")")
+ if count > hard_limit then
+ if debugging:GetBool() and can_print[ply] then
+ MsgC(Color(255,255,0), "[PAC3] : ") MsgC(Color(0,255,255), tostring(ply)) MsgC(Color(200,200,200), " TOO MANY ENTS (" .. count .. "). Beyond hard limit (".. hard_limit ..")\n")
+ end
+ return true
+ end
+ --if not game.SinglePlayer() then
+ if count > per_ply_limit:GetInt() * playercount then
+ if debugging:GetBool() and can_print[ply] then
+ MsgC(Color(255,255,0), "[PAC3] : ") MsgC(Color(0,255,255), tostring(ply)) MsgC(Color(200,200,200), " TOO MANY ENTS (" .. count .. "). Beyond per-player sending limit (".. per_ply_limit:GetInt() .." per player)\n")
+ end
+ return true
+ end
+ if count * playercount > math.min(hard_limit, per_ply*playercount) then
+ if debugging:GetBool() and can_print[ply] then
+ MsgC(Color(255,255,0), "[PAC3] : ") MsgC(Color(0,255,255), tostring(ply)) MsgC(Color(200,200,200), " TOO MANY ENTS (" .. count .. "). Beyond hard limit or player limit (" .. math.min(hard_limit, per_ply*playercount) .. ")\n")
+ end
+ return true
+ end
+ --end
+ return false
+ end
+
+ --consent check
+ local function PlayerAllowsCalcView(ply)
+ return grab_consents[ply] and calcview_consents[ply] --oops it's redundant but I prefer it this way
+ end
+
+ local function ApplyLockState(ent, bool, nocollide) --Change the movement states and reset some other angle-related things
+ if ulx and (ent.frozen or ent.jail) then return end
+ --the grab imposes MOVETYPE_NONE and no collisions
+ --reverting the state requires to reset the eyeang roll in case it was modified
+ if ent:IsPlayer() then
+ if bool then --apply lock
+ active_grabbed_ents[ent] = true
+ if nocollide then
+ ent:SetMoveType(MOVETYPE_NONE)
+ ent:SetCollisionGroup(COLLISION_GROUP_IN_VEHICLE)
+ else
+ ent:SetMoveType(MOVETYPE_WALK)
+ ent:SetCollisionGroup(COLLISION_GROUP_NONE)
+ end
+ else --revert
+ active_grabbed_ents[ent] = nil
+ if ent.default_movetype_reserved then
+ ent:SetMoveType(ent.default_movetype)
+ ent.default_movetype_reserved = nil
+ end
+ ent:SetCollisionGroup(COLLISION_GROUP_NONE)
+ local eyeang = ent:EyeAngles()
+ eyeang.r = 0
+ ent:SetEyeAngles(eyeang)
+ ent:SetPos(ent:GetPos() + Vector(0,0,10))
+ net.Start("pac_lock_imposecalcview")
+ net.WriteBool(false)
+ net.WriteVector(Vector(0,0,0))
+ net.WriteAngle(Angle(0,0,0))
+ net.Send(ent)
+ ent.has_calcview = false
+ end
+
+ elseif ent:IsNPC() then
+ if bool then
+ active_grabbed_ents[ent] = true
+ if nocollide then
+ ent:SetMoveType(MOVETYPE_NONE)
+ ent:SetCollisionGroup(COLLISION_GROUP_IN_VEHICLE)
+ else
+ ent:SetMoveType(MOVETYPE_STEP)
+ ent:SetCollisionGroup(COLLISION_GROUP_NONE)
+ end
+ else
+ active_grabbed_ents[ent] = nil
+ ent:SetMoveType(MOVETYPE_STEP)
+ ent:SetCollisionGroup(COLLISION_GROUP_NONE)
+ ent_ang = ent:GetAngles()
+ ent_ang.r = 0
+ ent:SetAngles(ent_ang)
+ end
+ end
+
+ if bool == nil then
+ for i,ply in pairs(player.GetAll()) do
+ if ply.grabbed_ents[ent] then
+ ply.grabbed_ents[ent] = nil
+ print(ent , "no longer grabbed by", ply)
+ end
+ end
+ elseif bool == false then
+ ent.lock_state_applied = false
+ end
+
+ ent:PhysWake()
+ ent:SetGravity(1)
+
+ end
+
+ local function maximized_ray_mins_maxs(startpos,endpos,padding)
+ local maxsx,maxsy,maxsz
+ local highest_sq_distance = 0
+ for xsign = -1, 1, 2 do
+ for ysign = -1, 1, 2 do
+ for zsign = -1, 1, 2 do
+ local distance_tried = (startpos + Vector(padding*xsign,padding*ysign,padding*zsign)):DistToSqr(endpos - Vector(padding*xsign,padding*ysign,padding*zsign))
+ if distance_tried > highest_sq_distance then
+ highest_sq_distance = distance_tried
+ maxsx,maxsy,maxsz = xsign,ysign,zsign
+ end
+ end
+ end
+ end
+ return Vector(padding*maxsx,padding*maxsy,padding*maxsz),Vector(padding*-maxsx,padding*-maxsy,padding*-maxsz)
+ end
+
+ local function AddDamageScale(ply, id,scale, part_uid)
+ ply.pac_damage_scalings = ply.pac_damage_scalings or {}
+ if id == "" then --no mod id = part uid mode, don't overwrite another part
+ ply.pac_damage_scalings[part_uid] = {scale = scale, id = id, uid = part_uid}
+ else --mod id = try to remove competing parts whose multipliers have the same mod id
+ for existing_uid,tbl in pairs(ply.pac_damage_scalings) do
+ if tbl.id == id then
+ ply.pac_damage_scalings[existing_uid] = nil
+ end
+ end
+ ply.pac_damage_scalings[part_uid] = {scale = scale, id = id, uid = part_uid}
+ end
+ end
+
+ local function FixMaxHealths(ply)
+ local biggest_health = 0
+ local biggest_armor = 0
+ local found_armor = false
+ local found_health = false
+
+ if ply.pac_healthmods then
+ for uid,tbl in pairs(ply.pac_healthmods) do
+ if tbl.maxhealth then biggest_health = math.max(biggest_health,tbl.maxhealth) found_health = true end
+ if tbl.maxarmor then biggest_armor = math.max(biggest_armor,tbl.maxarmor) found_armor = true end
+ end
+ end
+
+ if found_health then
+ ply:SetMaxHealth(biggest_health)
+ else
+ ply:SetMaxHealth(100)
+ ply:SetHealth(math.min(ply:Health(),100))
+ end
+ ply.pac_maxhealth = ply:GetMaxHealth()
+ if found_armor then
+ ply:SetMaxArmor(biggest_armor)
+ else
+ ply:SetMaxArmor(100)
+ ply:SetArmor(math.min(ply:Armor(),100))
+ end
+ ply.pac_maxhealth = ply:GetMaxArmor()
+ end
+
+ hook.Add("PlayerSpawn", "PAC_AutoMaxHealth_On_Respawn", function(ply)
+ FixMaxHealths(ply)
+ end)
+
+ local function GatherDamageScales(ent)
+ if not ent then return 0 end
+ if not ent:IsPlayer() then return 1 end
+ if not ent.pac_damage_scalings then return 1 end
+ local cumulative_dmg_scale = 1
+ for uid, tbl in pairs(ent.pac_damage_scalings) do
+ cumulative_dmg_scale = cumulative_dmg_scale * tbl.scale
+ end
+ return math.max(cumulative_dmg_scale,healthmod_minimum_dmgscaling:GetFloat())
+ end
+
+ --healthbars work with a 2 levels-deep table
+ --for each player, an index table (priority) to decide which layer is damaged first
+ --for each layer, one table for each part uid
+ --for each uid, we have the current uid bar cluster's health value
+ --instead of keeping track of every bar, it will update the status with a remainder calculation
+
+ --ply.pac_healthbars
+ --ply.pac_healthbars[layer]
+ --ply.pac_healthbars[layer][part_uid] = healthvalue
+
+ local function UpdateHealthBars(ply, num, barsize, layer, absorbfactor, part_uid, follow, counted_hits, no_overflow)
+ local existing_uidlayer = true
+ local healthvalue = 0
+ if ply.pac_healthbars == nil then
+ existing_uidlayer = false
+ ply.pac_healthbars = {}
+ end
+ if ply.pac_healthbars[layer] == nil then
+ existing_uidlayer = false
+ ply.pac_healthbars[layer] = {}
+ end
+ if ply.pac_healthbars[layer][part_uid] == nil then
+ existing_uidlayer = false
+ ply.pac_healthbars[layer][part_uid] = num*barsize
+ healthvalue = num*barsize
+ end
+
+ if (not existing_uidlayer) or follow then
+ healthvalue = num*barsize
+ end
+
+ ply.pac_healtbar_uid_absorbfactor = ply.pac_healtbar_uid_absorbfactor or {}
+ ply.pac_healtbar_uid_absorbfactor[part_uid] = absorbfactor
+
+ ply.pac_healtbar_uid_info = ply.pac_healtbar_uid_info or {}
+ ply.pac_healtbar_uid_info[part_uid] = {
+ absorb_factor = absorbfactor,
+ counted_hits = counted_hits,
+ no_overflow = no_overflow
+ }
+
+ if num == 0 then --remove
+ ply.pac_healthbars[layer] = nil
+ ply.pac_healtbar_uid_info[part_uid].absorbfactor = nil
+ elseif num > 0 then --add if follow or created
+ if follow or not existing_uidlayer then
+ ply.pac_healthbars[layer][part_uid] = healthvalue
+ ply.pac_healtbar_uid_info[part_uid].absorbfactor = absorbfactor
+ end
+ end
+ for checklayer,tbl in pairs(ply.pac_healthbars) do
+ local layertotal = 0
+ for uid,value in pairs(tbl) do
+ layertotal = layertotal + value
+ if layer ~= checklayer and part_uid == uid then
+ ply.pac_healthbars[checklayer][uid] = nil
+ if table.IsEmpty(ply.pac_healthbars[checklayer]) then ply.pac_healthbars[checklayer] = nil end
+ end
+ end
+ if layertotal == 0 then ply.pac_healthbars[checklayer] = nil end
+ end
+ end
+
+ local function UpdateHealthBarsFromCMD(ply, action, num, part_uid)
+ if ply.pac_healthbars == nil then return end
+
+ local target_tbl
+ for checklayer,tbl in pairs(ply.pac_healthbars) do
+ if tbl[part_uid] ~= nil then
+ target_tbl = tbl
+ end
+ end
+
+ if target_tbl == nil then return end
+
+ --actions: set, add, subtract, replenish, remove
+ if action == "set" then
+ target_tbl[part_uid] = num
+ elseif action == "add" then
+ target_tbl[part_uid] = math.max(target_tbl[part_uid] + num,0)
+ elseif action == "subtract" then
+ target_tbl[part_uid] = math.max(target_tbl[part_uid] - num,0)
+ elseif action == "remove" then
+ target_tbl[part_uid] = nil
+ end
+ end
+
+ local function GatherExtraHPBars(ply, filter)
+ if ply.pac_healthbars == nil then return 0,nil end
+
+ local built_tbl = {}
+ local total_hp_value = 0
+
+ for layer,tbl in pairs(ply.pac_healthbars) do
+ built_tbl[layer] = {}
+ local layer_total = 0
+ for uid,value in pairs(tbl) do
+ if uid == filter then continue end
+ built_tbl[layer][uid] = value
+ total_hp_value = total_hp_value + value
+ layer_total = layer_total + value
+ end
+ end
+ return total_hp_value,built_tbl
+
+ end
+
+ --simulate on a healthbar layers copy
+ local function GetPredictedHPBarDamage(ply, dmg)
+ local BARS_COPY = {}
+ if ply.pac_healthbars then
+ BARS_COPY = table.Copy(ply.pac_healthbars)
+ else --this can happen with non-player ents
+ return dmg,nil,nil
+ end
+
+ local remaining_dmg = dmg or 0
+ local surviving_layer = 15
+ local total_hp_value,built_tbl = GatherExtraHPBars(ply)
+ local side_effect_dmg = 0
+
+ if not built_tbl or total_hp_value == 0 then --no shields
+ return dmg,nil,nil
+ end
+
+ for layer=15,0,-1 do --go progressively inward in the layers
+ if BARS_COPY[layer] then
+ surviving_layer = layer
+ for uid,value in pairs(BARS_COPY[layer]) do --check the healthbars by uid
+
+ if value > 0 then --skip 0 HP healthbars
+
+ local remainder = math.max(0,remaining_dmg - BARS_COPY[layer][uid])
+
+ local breakthrough_dmg = math.min(remaining_dmg, value)
+
+ if remaining_dmg > value then --break through one of the uid clusters
+ surviving_layer = layer - 1
+ BARS_COPY[layer][uid] = 0
+ else
+ BARS_COPY[layer][uid] = math.max(0, value - remaining_dmg)
+ end
+
+ local absorbfactor = ply.pac_healtbar_uid_info[uid].absorbfactor
+ side_effect_dmg = side_effect_dmg + breakthrough_dmg * absorbfactor
+
+ remaining_dmg = math.max(0,remaining_dmg - value)
+ end
+
+ end
+ end
+ end
+ return remaining_dmg,surviving_layer,side_effect_dmg
+ end
+
+ --do the calculation and reduce the player's underlying values
+ local function GetHPBarDamage(ply, dmg)
+ local remaining_dmg = dmg or 0
+ local surviving_layer = 15
+ local total_hp_value,built_tbl = GatherExtraHPBars(ply)
+ local side_effect_dmg = 0
+
+ if not built_tbl or total_hp_value == 0 then --no shields
+ return dmg,nil,nil
+ end
+
+ for layer=15,0,-1 do --go progressively inward in the layers
+ if ply.pac_healthbars[layer] then
+ surviving_layer = layer
+ for uid,value in pairs(ply.pac_healthbars[layer]) do --check the healthbars by uid
+
+ if value > 0 then --skip 0 HP healthbars
+ local counted_hits_mode = ply.pac_healtbar_uid_info[uid].counted_hits
+
+ local absorbfactor = ply.pac_healtbar_uid_info[uid].absorbfactor
+ local breakthrough_dmg
+
+ if counted_hits_mode then
+ ply.pac_healthbars[layer][uid] = ply.pac_healthbars[layer][uid] - 1
+ breakthrough_dmg = remaining_dmg
+ remaining_dmg = 0
+ else
+ --local remainder = math.max(0,remaining_dmg - ply.pac_healthbars[layer][uid])
+
+ --if the dmg is more than health value, we will have a breakthrough damage
+ breakthrough_dmg = math.min(remaining_dmg, value)
+
+ if remaining_dmg > value then --break through one of the uid clusters
+ surviving_layer = layer - 1
+ ply.pac_healthbars[layer][uid] = 0
+ else --subtracting the health now
+ ply.pac_healthbars[layer][uid] = math.max(0, value - remaining_dmg)
+ end
+ if ply.pac_healtbar_uid_info[uid].no_overflow then
+ remaining_dmg = 0
+ breakthrough_dmg = 0
+ else
+ remaining_dmg = math.max(0,remaining_dmg - value)
+ end
+ end
+ side_effect_dmg = side_effect_dmg + breakthrough_dmg * absorbfactor
+ end
+
+ end
+ end
+ end
+ return remaining_dmg,surviving_layer,side_effect_dmg
+ end
+
+ local function SendUpdateHealthBars(target)
+ if not target:IsPlayer() or not target.pac_healthbars then return end
+ local table_copy = {}
+ local layers = 0
+
+ for layer=0,15,1 do --ok so we're gonna compress it
+ if not target.pac_healthbars[layer] then continue end
+ local tbl = target.pac_healthbars[layer]
+ layers = layer
+ table_copy[layer] = {}
+ for uid, value in pairs(tbl) do
+ table_copy[layer][string.sub(uid, 1, 8)] = math.Round(value)
+ end
+ end
+ --PrintTable(table_copy)
+ net.Start("pac_update_healthbars")
+ net.WriteEntity(target)
+ net.WriteUInt(layers, 4)
+ for i=0,layers,1 do
+ --PrintTable(table_copy)
+ if not table_copy[i] then
+ net.WriteBool(true)--skip
+ continue
+ elseif not table.IsEmpty(table_copy[i]) then
+ net.WriteBool(false)--data exists
+ end
+ net.WriteUInt(math.Clamp(table.Count(table_copy[i]),0,15), 4)
+ for uid, value in pairs(table_copy[i]) do
+ net.WriteString(uid) --partial UID was written before
+ net.WriteUInt(value,24)
+ end
+ end
+ net.Broadcast()
+ end
+
+ --healthbars work with a 2 levels-deep table
+ --for each player, an index table (priority) to decide which layer is damaged first
+ --for each layer, one table for each part uid
+ --for each uid, we have the current uid bar cluster's health value
+ --instead of keeping track of every bar, it will update the status with a remainder calculation
+
+ --ply.pac_healthbars
+ --ply.pac_healthbars[layer]
+ --ply.pac_healthbars[layer][part_uid] = healthvalue
+
+
+ --apply hitscan consents, eat into extra healthbars first and calculate final damage multipliers from pac3
+ hook.Add( "EntityTakeDamage", "ApplyPACDamageModifiers", function( target, dmginfo )
+ if target:IsPlayer() then
+ local cumulative_mult = GatherDamageScales(target)
+
+ dmginfo:ScaleDamage(cumulative_mult)
+ local pretotal_hp_value,prebuilt_tbl = GatherExtraHPBars(target)
+ local remaining_dmg,surviving_layer,side_effect_dmg = GetHPBarDamage(target, dmginfo:GetDamage())
+
+ if IsValid(dmginfo:GetInflictor()) then
+ if dmginfo:GetInflictor():GetClass() == "pac_bullet_emitter" and hitscan_consents[target] == false then --unconsenting for pac hitscans = no damage, exit now
+ return true
+ end
+ end
+
+ local total_hp_value,built_tbl = GatherExtraHPBars(target)
+ if surviving_layer == nil or (total_hp_value == 0 and pretotal_hp_value == total_hp_value) or not built_tbl then --no shields = use the dmginfo base damage scaled with the cumulative mult
+
+ if cumulative_mult < 0 then
+ target:SetHealth(math.floor(math.Clamp(target:Health() + math.abs(dmginfo:GetDamage()),0,target:GetMaxHealth())))
+ return true
+ else
+ dmginfo:SetDamage(remaining_dmg)
+ --if target.pac_healthbars then SendUpdateHealthBars(target) end
+ end
+
+ else --shields = use the calculated cumulative side effect damage from each uid's related absorbfactor
+
+ if side_effect_dmg < 0 then
+ target:SetHealth(math.floor(math.Clamp(target:Health() + math.abs(side_effect_dmg),0,target:GetMaxHealth())))
+ SendUpdateHealthBars(target)
+ return true
+ else
+ dmginfo:SetDamage(side_effect_dmg + remaining_dmg)
+ SendUpdateHealthBars(target)
+ end
+
+ end
+
+ end
+ end)
+
+ local function MergeTargetsByID(tbl1, tbl2)
+ for i,v in ipairs(tbl2) do
+ tbl1[v:EntIndex()] = v
+ end
+ end
+
+ local function ProcessDamagesList(ents_hits, dmg_info, tbl, pos, ang, ply)
+ local base_damage = tbl.Damage
+ local ent_count = 0
+ local ply_count = 0
+ local ply_prog_count = 0
+ for i,v in pairs(ents_hits) do
+ if not (v:IsPlayer() or Is_NPC(v)) and not tbl.PointEntities then ents_hits[i] = nil continue end
+ if v.CPPICanDamage and not v:CPPICanDamage(ply) then ents_hits[i] = nil continue end --CPPI check on the player
+ if v:IsConstraint() then ents_hits[i] = nil continue end
+
+ if not NPCDispositionAllowsIt(ply, v) then ents_hits[i] = nil continue end
+ if NPCDispositionIsFilteredOut(ply,v, tbl.FilterFriendlies, tbl.FilterNeutrals, tbl.FilterHostiles) then ents_hits[i] = nil end
+
+ if pre_excluded_ent_classes[v:GetClass()] or v:IsWeapon() or (v:IsNPC() and not tbl.NPC) or ((v ~= ply and v:IsPlayer() and not tbl.Players) and not (tbl.AffectSelf and v == ply)) then ents_hits[i] = nil continue
+ else
+ ent_count = ent_count + 1
+ --print(v, "counted")
+ if v:IsPlayer() then ply_count = ply_count + 1 end
+ end
+ end
+
+
+ --dangerous conditions: absurd amounts of entities, damaging a large percentage of the server's players beyond a certain point
+ if TooManyEnts(ent_count, ply) or ((ply_count) > 12 and (ply_count > player_fraction:GetFloat() * player.GetCount())) then
+ return false,false,nil,{},{}
+ end
+
+ local pac_sv_damage_zone_allow_dissolve = GetConVar("pac_sv_damage_zone_allow_dissolve"):GetBool()
+ local pac_sv_prop_protection = global_combat_prop_protection:GetBool()
+
+ local inflictor = dmg_info:GetInflictor() or ply
+ local attacker = dmg_info:GetAttacker() or ply
+
+ local kill = false --whether a kill was done
+ local hit = false --whether a hit was done
+ local max_dmg = 0 --the max damage applied to targets. it should give the same damage by default, but I'm accounting for targets that can modify their damage
+ local successful_hit_ents = {}
+ local successful_kill_ents = {}
+
+ local bullet = {}
+ bullet.Src = pos + ang:Forward()
+ bullet.Dir = ang:Forward()*50000
+ bullet.Damage = -1
+ bullet.Force = 0
+ bullet.Entity = dmg_info:GetAttacker()
+
+ --the function to determine if we can dissolve, based on policy and setting factors
+ local function IsDissolvable(ent)
+ local owner = Try_CPPIGetOwner(ent)
+
+ local prop_protected_final
+ if IsValid(owner) then --created entities should be fine
+ prop_protected_final = prop_protected and owner:IsPlayer() and damage_zone_consents[owner] == false
+ else --players and world props could nil out
+ prop_protected_final = false
+ end
+ if not pac_sv_damage_zone_allow_dissolve then return false end
+ local dissolvable = true
+ local prop_protected, reason = IsPropProtected(ent, attacker)
+
+ if ent:IsPlayer() then
+ if not kill then dissolvable = false
+ elseif damage_zone_consents[ent] == false then dissolvable = false end
+ elseif inflictor == ent then
+ dissolvable = false --do we allow that?
+ end
+ if ent:IsWeapon() and IsValid(owner) then
+ dissolvable = false
+ end
+ if ent:CreatedByMap() then
+ dissolvable = false
+ if ent:GetClass() == "prop_physics" then dissolvable = true end
+ end
+ if damageable_point_ent_classes[ent:GetClass()] == false then
+ dissolvable = false
+ end
+ if prop_protected_final then
+ dissolvable = false
+ end
+ return dissolvable
+ end
+
+ local dissolver_entity = NULL
+ local function dissolve(target, attacker, typ)
+ local dissolver_ent = ents.Create("env_entity_dissolver")
+ dissolver_ent:Spawn()
+ target:SetName(tostring({}))
+ dissolver_ent:SetKeyValue("dissolvetype", tostring(typ))
+ dissolver_ent:Fire("Dissolve", target:GetName())
+ timer.Simple(5, function() SafeRemoveEntity(dissolver_ent) end)
+ dissolver_entity = dissolver_ent
+ end
+
+ --the giga function to determine if we can damage
+ local function DMGAllowed(ent)
+ if ent:Health() == 0 and not (string.find(tbl.DamageType, "dissolve")) then return false end --immediately exclude entities with 0 health, except if we want to dissolve
+
+
+ local canhit = false --whether the policies allow the hit
+ local prop_protected_consent
+ local contraption = IsPossibleContraptionEntity(ent)
+ local bot_exception = false
+ if ent:IsPlayer() then
+ if ent:IsBot() then bot_exception = true end
+ end
+
+ local owner = Try_CPPIGetOwner(ent)
+ local target_ply
+ if IsValid(owner) then --created entities should be fine
+ target_ply = owner
+ prop_protected_consent = owner ~= inflictor and ent ~= inflictor and owner:IsPlayer() and damage_zone_consents[owner] == false
+ else --players and world props could nil out
+ prop_protected_consent = false
+ if ent:IsPlayer() then
+ target_ply = ent
+ end
+ end
+
+ --first pass: entity class blacklist
+
+ if IsEntity(ent) and ((damageable_point_ent_classes[ent:GetClass()] ~= false) or ((damageable_point_ent_classes[ent:GetClass()] == nil) or (damageable_point_ent_classes[ent:GetClass()] == true))) then
+ --second pass: the damagezone's settings
+ --1.player hurt self if asked
+ local is_player = ent:IsPlayer()
+ local is_physics = (physics_point_ent_classes[ent:GetClass()] or string.find(ent:GetClass(),"item_") or string.find(ent:GetClass(),"ammo_"))
+ local is_npc = Is_NPC(ent)
+
+ if (tbl.AffectSelf) and ent == inflictor then
+ canhit = true
+ --2.main target types : players, NPC, point entities
+ elseif --one of the base classes
+ (damageable_point_ent_classes[ent:GetClass()] ~= false) --non-blacklisted class
+ and --enforce prop protection
+ (bot_exception or (owner == inflictor or ent == inflictor or (pac_sv_prop_protection and damage_zone_consents[target_ply] ~= false) or not pac_sv_prop_protection))
+ then
+
+ if is_player then
+ if tbl.Players then
+ canhit = true
+ --rules for players:
+ --self can always hurt itself if asked to
+ if (ent == inflictor and not tbl.AffectSelf) then
+ canhit = false --self shouldn't hurt itself if asked not to
+ elseif (damage_zone_consents[ent] == true) or ent:IsBot() then
+ canhit = true --other players need to consent, bots don't care about it
+ --other players that didn't consent are excluded
+ else
+ canhit = false
+ end
+ end
+ elseif is_npc then
+ if tbl.NPC then
+ canhit = true
+ end
+ elseif tbl.PointEntities and (damageable_point_ent_classes[ent:GetClass()] == true) then
+ canhit = true
+ end
+
+ --apply prop protection
+ if (IsPropProtected(ent, inflictor) and IsValid(owner) and damage_zone_consents[target_ply]) or prop_protected_consent or (ent.CPPICanDamage and not ent:CPPICanDamage(ply)) then
+ canhit = false
+ end
+
+ end
+
+ end
+
+ return canhit
+ end
+
+ local function IsLiving(ent) --players and NPCs
+ return ent:IsPlayer() or Is_NPC(ent)
+ end
+
+ --final action to apply the DamageInfo
+ local function DoDamage(ent)
+ --add the max hp-scaled damage calculated with this entity's max health
+ tbl.Damage = base_damage + tbl.MaxHpScaling * ent:GetMaxHealth()
+ dmg_info:SetDamage(tbl.Damage)
+ --we'll need to find out whether the damage will crack open a player's extra bars
+ local de_facto_dmg = GetPredictedHPBarDamage(ent, tbl.Damage)
+
+ local distance = (ent:GetPos()):Distance(pos)
+
+ local fraction = math.pow(math.Clamp(1 - distance / math.Clamp(math.max(tbl.Radius, tbl.Length),1,50000),0,1),tbl.DamageFalloffPower)
+
+ if tbl.DamageFalloff then
+ dmg_info:SetDamage(fraction * tbl.Damage)
+ end
+
+ table.insert(successful_hit_ents,ent)
+ --fire bullets if asked
+ local ents2 = {inflictor}
+ if tbl.Bullet then
+ for _,v in ipairs(ents_hits) do
+ if v ~= ent then table.insert(ents2,v) end
+ end
+
+ traceresult = util.TraceLine({filter = ents2, start = pos, endpos = pos + 50000*(ent:WorldSpaceCenter() - dmg_info:GetAttacker():WorldSpaceCenter())})
+
+ bullet.Dir = traceresult.Normal
+ bullet.Src = traceresult.HitPos + traceresult.HitNormal*5
+ dmg_info:GetInflictor():FireBullets(bullet)
+
+ end
+
+ --this may benefit from some flattening treatment, lotta pyramids over here
+ if tbl.DamageType == "heal" and ent.Health then
+ if ent:Health() < ent:GetMaxHealth() then
+ if tbl.ReverseDoNotKill then --don't heal if health is below critical
+ if ent:Health() > tbl.CriticalHealth then --default behavior
+ ent:SetHealth(math.min(ent:Health() + tbl.Damage, math.max(ent:Health(), ent:GetMaxHealth())))
+ end --else do nothing
+ else
+ if tbl.DoNotKill then --stop healing at the critical health
+ if ent:Health() < tbl.CriticalHealth then
+ ent:SetHealth(math.min(ent:Health() + tbl.Damage, math.min(tbl.CriticalHealth, ent:GetMaxHealth())))
+ end --else do nothing, we're already above critical
+ else
+ ent:SetHealth(math.min(ent:Health() + tbl.Damage, math.max(ent:Health(), ent:GetMaxHealth())))
+ end
+ end
+ end
+ elseif tbl.DamageType == "armor" and ent.Armor then
+ if ent:Armor() < ent:GetMaxArmor() then
+ if tbl.ReverseDoNotKill then --don't heal if armor is below critical
+ if ent:Armor() > tbl.CriticalHealth then --default behavior
+ ent:SetArmor(math.min(ent:Armor() + tbl.Damage, math.max(ent:Armor(), ent:GetMaxArmor())))
+ end --else do nothing
+ else
+ if tbl.DoNotKill then --stop healing at the critical health
+ if ent:Armor() < tbl.CriticalHealth then
+ ent:SetArmor(math.min(ent:Armor() + tbl.Damage, math.min(tbl.CriticalHealth, ent:GetMaxArmor())))
+ end --else do nothing, we're already above critical
+ else
+ ent:SetArmor(math.min(ent:Armor() + tbl.Damage, math.max(ent:Armor(), ent:GetMaxArmor())))
+ end
+ end
+ end
+ else
+ --only "living" entities can be killed, and we checked generic entities with a ghost 0 health previously
+
+ --now, after checking the de facto damage after extra healthbars, there's a 80% absorbtion ratio of armor.
+ --so, the kill condition is either:
+ --if damage is 500% of health (no amount will save you, because the remainder of 80% means death)
+ --if damage is more than 125% of armor, and damage is more than health+armor
+ if IsLiving(ent) and ent:Health() - de_facto_dmg <= 0 then
+ if ent.Armor then
+
+ if not (de_facto_dmg > 5*ent:Health()) and not (de_facto_dmg > 1.25*ent:Armor() and de_facto_dmg > ent:Health() + ent:Armor()) then
+ kill = false
+ else
+ kill = true
+ end
+ else
+ kill = true
+ end
+
+ end
+ if tbl.DoNotKill then
+ kill = false --durr
+ end
+ if kill then
+ table.insert(successful_kill_ents,ent)
+ ent.pac_damagezone_need_send_ragdoll = true
+ ent.pac_damagezone_killer = ply
+ end
+
+ --remove weapons on kill if asked
+ if kill and not ent:IsPlayer() and tbl.RemoveNPCWeaponsOnKill and pac_sv_damage_zone_allow_dissolve then
+ if ent:IsNPC() then
+ if #ent:GetWeapons() >= 1 then
+ for _,wep in pairs(ent:GetWeapons()) do
+ SafeRemoveEntity(wep)
+ end
+ end
+ end
+ end
+
+ if tbl.ReverseDoNotKill then
+ --don't damage if health is above critical
+ if ent:Health() < tbl.CriticalHealth then
+ if string.find(tbl.DamageType, "dissolve") and IsDissolvable(ent) then
+ dissolve(ent, dmg_info:GetInflictor(), damage_types[tbl.DamageType])
+ end
+ dmg_info:SetDamagePosition(ent:NearestPoint(pos))
+ dmg_info:SetReportedPosition(pos)
+ ent:TakeDamageInfo(dmg_info)
+ max_dmg = math.max(max_dmg, dmg_info:GetDamage())
+ end
+ else
+ --leave at a critical health
+ if tbl.DoNotKill then
+ local dmg_info2 = DamageInfo()
+
+ dmg_info2:SetDamagePosition(ent:NearestPoint(pos))
+ dmg_info2:SetReportedPosition(pos)
+ dmg_info2:SetDamage( math.min(ent:Health() - tbl.CriticalHealth, tbl.Damage))
+ dmg_info2:IsBulletDamage(tbl.Bullet)
+ dmg_info2:SetDamageForce(Vector(0,0,0))
+
+ if IsValid(attacker) then dmg_info2:SetAttacker(attacker) end
+
+ if IsValid(inflictor) then dmg_info2:SetInflictor(inflictor) end
+
+ ent:TakeDamageInfo(dmg_info2)
+ max_dmg = math.max(max_dmg, dmg_info2:GetDamage())
+ --finally we reached the normal damage event!
+ else
+ if string.find(tbl.DamageType, "dissolve") and IsDissolvable(ent) then
+ dissolve(ent, dmg_info:GetInflictor(), damage_types[tbl.DamageType])
+ end
+ dmg_info:SetDamagePosition(ent:NearestPoint(pos))
+ dmg_info:SetReportedPosition(pos)
+ ent:TakeDamageInfo(dmg_info)
+ max_dmg = math.max(max_dmg, dmg_info:GetDamage())
+ end
+ end
+ end
+
+ if tbl.DamageType == "fire" then ent:Ignite(5) end
+ end
+
+ --the forward bullet, if applicable and no entity is found
+ if ent_count == 0 then
+ if tbl.Bullet then
+ dmg_info:GetInflictor():FireBullets(bullet)
+ end
+ return hit,kill,dmg,successful_hit_ents,successful_kill_ents
+ end
+
+ --look through each entity
+ for _,ent in pairs(ents_hits) do
+ local canhit = DMGAllowed(ent)
+ local oldhp = ent:Health()
+ if canhit then
+ if ent:IsPlayer() and ply_count > 5 then
+ --jank fix to delay players damage in case they die all at once overflowing the reliable buffer
+ timer.Simple(ply_prog_count / 32, function() DoDamage(ent) end)
+ ply_prog_count = ply_prog_count + 1
+ else
+ if tbl.DOTMode then
+ local counts = tbl.NoInitialDOT and tbl.DOTCount or tbl.DOTCount-1
+ local timer_entid = tbl.UniqueID .. "_" .. ent:GetClass() .. "_" .. ent:EntIndex()
+ if counts <= 0 then --nuh uh, timer 0 means infinite repeat
+ timer.Remove(timer_entid)
+ else
+ if timer.Exists(timer_entid) then
+ timer.Adjust(tbl.UniqueID, tbl.DOTTime, counts)
+ else
+ timer.Create(timer_entid, tbl.DOTTime, counts, function()
+ if not IsValid(ent) then timer.Remove(timer_entid) return end
+ DoDamage(ent)
+ end)
+ end
+ end
+
+ end
+ if not tbl.NoInitialDOT then DoDamage(ent) end
+ end
+ end
+ if not hit and (oldhp > 0 and canhit) then hit = true end
+ end
+ if IsValid(ent) then
+ if kill then
+ timer.Remove(tbl.UniqueID .. "_" .. ent:GetClass() .. "_" .. ent:EntIndex())
+ end
+ return
+ end
+
+ return hit,kill,dmg,successful_hit_ents,successful_kill_ents
+ end
+
+
+ local hitbox_ids = {
+ ["Box"] = 1,
+ ["Cube"] = 2,
+ ["Sphere"] = 3,
+ ["Cylinder"] = 4,
+ ["CylinderHybrid"] = 5,
+ ["CylinderSpheres"] = 6,
+ ["Cone"] = 7,
+ ["ConeHybrid"] = 8,
+ ["ConeSpheres"] = 9,
+ ["Ray"] = 10
+ }
+
+ local damage_ids = {
+ generic = 0, --generic damage
+ crush = 1, --caused by physics interaction
+ bullet = 2, --bullet damage
+ slash = 3, --sharp objects, such as manhacks or other npcs attacks
+ burn = 4, --damage from fire
+ vehicle = 5, --hit by a vehicle
+ fall = 6, --fall damage
+ blast = 7, --explosion damage
+ club = 8, --crowbar damage
+ shock = 9, --electrical damage, shows smoke at the damage position
+ sonic = 10, --sonic damage,used by the gargantua and houndeye npcs
+ energybeam = 11, --laser
+ nevergib = 12, --don't create gibs
+ alwaysgib = 13, --always create gibs
+ drown = 14, --drown damage
+ paralyze = 15, --same as dmg_poison
+ nervegas = 16, --neurotoxin damage
+ poison = 17, --poison damage
+ acid = 18, --
+ airboat = 19, --airboat gun damage
+ blast_surface = 20, --this won't hurt the player underwater
+ buckshot = 21, --the pellets fired from a shotgun
+ direct = 22, --
+ dissolve = 23, --forces the entity to dissolve on death
+ drownrecover = 24, --damage applied to the player to restore health after drowning
+ physgun = 25, --damage done by the gravity gun
+ plasma = 26, --
+ prevent_physics_force = 27, --
+ radiation = 28, --radiation
+ removenoragdoll = 29, --don't create a ragdoll on death
+ slowburn = 30, --
+
+ fire = 31, -- ent:Ignite(5)
+
+ -- env_entity_dissolver
+ dissolve_energy = 32,
+ dissolve_heavy_electrical = 33,
+ dissolve_light_electrical = 34,
+ dissolve_core_effect = 35,
+
+ heal = 36,
+ armor = 37,
+ }
+
+ local tracer_ids = {
+ ["Tracer"] = 1,
+ ["AR2Tracer"] = 2,
+ ["HelicopterTracer"] = 3,
+ ["AirboatGunTracer"] = 4,
+ ["AirboatGunHeavyTracer"] = 5,
+ ["GaussTracer"] = 6,
+ ["HunterTracer"] = 7,
+ ["StriderTracer"] = 8,
+ ["GunshipTracer"] = 9,
+ ["ToolTracer"] = 10,
+ ["LaserTracer"] = 11
+ }
+
+ --second stage of force: apply
+ local function ProcessForcesList(ents_hits, tbl, pos, ang, ply)
+ local ent_count = 0
+ for i,v in pairs(ents_hits) do
+ if v.CPPICanPickup and not v:CPPICanPickup(ply) then ents_hits[i] = nil end
+ if v.CPPICanPunt and not v:CPPICanPunt(ply) then ents_hits[i] = nil end
+ if v:IsConstraint() then ents_hits[i] = nil end
+
+ if v == ply then
+ if not tbl.AffectPlayerOwner then ents_hits[i] = nil end
+ elseif v == tbl.RootPartOwner then
+ if (not tbl.AffectSelf) and v == tbl.RootPartOwner then ents_hits[i] = nil end
+ end
+
+ if pre_excluded_ent_classes[v:GetClass()] or (Is_NPC(v) and not tbl.NPC) or (v:IsPlayer() and not tbl.Players and not (v == ply and tbl.AffectPlayerOwner)) then ents_hits[i] = nil
+ end
+ if ents_hits[i] ~= nil then
+ ent_count = ent_count + 1
+ end
+ end
+ if TooManyEnts(ent_count, ply) and not ((tbl.AffectSelf or tbl.AffectPlayerOwner) and not tbl.Players and not tbl.NPC and not tbl.PhysicsProps and not tbl.PointEntities) then return end
+ for _,ent in pairs(ents_hits) do
+ local phys_ent
+ local ent_getphysobj = ent:GetPhysicsObject()
+ local owner = Try_CPPIGetOwner(ent)
+ local is_player = ent:IsPlayer()
+ local is_physics = (physics_point_ent_classes[ent:GetClass()] or string.find(ent:GetClass(),"item_") or string.find(ent:GetClass(),"ammo_") or (ent:IsWeapon() and not IsValid(ent:GetOwner())))
+ local is_npc = Is_NPC(ent)
+ if (ent ~= tbl.RootPartOwner or (tbl.AffectSelf and ent == tbl.RootPartOwner) or (tbl.AffectPlayerOwner and ent == ply))
+ and (
+ is_player
+ or is_npc
+ or is_physics
+ or IsValid( ent_getphysobj )
+ ) then
+
+ local is_phys = true
+ if ent_getphysobj ~= nil then
+ phys_ent = ent_getphysobj
+ if is_npc then
+ phys_ent = ent
+ end
+ else
+ phys_ent = ent
+ is_phys = false
+ end
+
+ local oldvel
+
+ if IsValid(phys_ent) then
+ oldvel = phys_ent:GetVelocity()
+ else
+ oldvel = Vector(0,0,0)
+ end
+
+
+ local addvel = Vector(0,0,0)
+ local add_angvel = Vector(0,0,0)
+
+ local ent_center = ent:WorldSpaceCenter() or ent:GetPos()
+
+ local dir = ent_center - pos --part
+ local dir2 = ent_center - tbl.Locus_pos--locus
+
+ local dist_multiplier = 1
+ local damping_dist_mult = 1
+ local up_mult = 1
+ local distance = (ent_center - pos):Length()
+ local height_delta = pos.z + tbl.LevitationHeight - ent_center.z
+
+ --what it do
+ --if delta is -100 (ent is lower than the desired height), that means +100 adjustment direction
+ --height decides how much to knee the force until it equalizes at 0
+ --clamp the delta to the ratio levitation height
+
+ if tbl.Levitation then
+ up_mult = math.Clamp(height_delta / (5 + math.abs(tbl.LevitationHeight)),-1,1)
+ end
+
+ if tbl.BaseForceAngleMode == "Radial" then --radial on self
+ addvel = dir:GetNormalized() * tbl.BaseForce
+ elseif tbl.BaseForceAngleMode == "Locus" then --radial on locus
+ addvel = dir2:GetNormalized() * tbl.BaseForce
+ elseif tbl.BaseForceAngleMode == "Local" then --forward on self
+ addvel = ang:Forward() * tbl.BaseForce
+ end
+
+ if tbl.VectorForceAngleMode == "Global" then --global
+ addvel = addvel + tbl.AddedVectorForce
+ elseif tbl.VectorForceAngleMode == "Local" then --local on self
+ addvel = addvel
+ +ang:Forward()*tbl.AddedVectorForce.x
+ +ang:Right()*tbl.AddedVectorForce.y
+ +ang:Up()*tbl.AddedVectorForce.z
+
+ elseif tbl.VectorForceAngleMode == "Radial" then --relative to locus or self
+ ang2 = dir:Angle()
+ addvel = addvel
+ +ang2:Forward()*tbl.AddedVectorForce.x
+ +ang2:Right()*tbl.AddedVectorForce.y
+ +ang2:Up()*tbl.AddedVectorForce.z
+ elseif tbl.VectorForceAngleMode == "RadialNoPitch" then --relative to locus or self
+ dir.z = 0
+ ang2 = dir:Angle()
+ addvel = addvel
+ +ang2:Forward()*tbl.AddedVectorForce.x
+ +ang2:Right()*tbl.AddedVectorForce.y
+ +ang2:Up()*tbl.AddedVectorForce.z
+ end
+
+ if tbl.TorqueMode == "Global" then
+ add_angvel = tbl.Torque
+ elseif tbl.TorqueMode == "Local" then
+ add_angvel = ang:Forward()*tbl.Torque.x + ang:Right()*tbl.Torque.y + ang:Up()*tbl.Torque.z
+ elseif tbl.TorqueMode == "TargetLocal" then
+ add_angvel = tbl.Torque
+ elseif tbl.TorqueMode == "Radial" then
+ ang2 = dir:Angle()
+ addvel = ang2:Forward()*tbl.Torque.x + ang2:Right()*tbl.Torque.y + ang2:Up()*tbl.Torque.z
+ end
+
+ local islocaltorque = tbl.TorqueMode == "TargetLocal"
+
+ local mass = 1
+ if IsValid(phys_ent) then
+ if phys_ent.GetMass then
+ phys_ent:GetMass()
+ end
+ end
+ if is_phys and tbl.AccountMass then
+ if not is_npc then
+ addvel = addvel * (1 / math.max(mass,0.1))
+ else
+ addvel = addvel
+ end
+ add_angvel = add_angvel * (1 / math.max(mass,0.1))
+ end
+
+ if tbl.Falloff then
+ dist_multiplier = math.Clamp(1 - distance / math.max(tbl.Radius, tbl.Length),0,1)
+ end
+ if tbl.ReverseFalloff then
+ dist_multiplier = 1 - math.Clamp(1 - distance / math.max(tbl.Radius, tbl.Length),0,1)
+ end
+
+ if tbl.DampingFalloff then
+ damping_dist_mult = math.Clamp(1 - distance / math.max(tbl.Radius, tbl.Length),0,1)
+ end
+ if tbl.DampingReverseFalloff then
+ damping_dist_mult = 1 - math.Clamp(1 - distance / math.max(tbl.Radius, tbl.Length),0,1)
+ end
+ damping_dist_mult = damping_dist_mult
+ local final_damping = 1 - (tbl.Damping * damping_dist_mult)
+
+ if tbl.Levitation then
+ addvel.z = addvel.z * up_mult
+ end
+
+ addvel = addvel * dist_multiplier
+ add_angvel = add_angvel * dist_multiplier
+
+ local unconsenting_owner = owner ~= ply and force_consents[owner] == false
+
+ if is_player then
+ if tbl.Players or (ent == ply and tbl.AffectPlayerOwner) then
+ if (ent ~= ply and force_consents[ent] ~= false) or (ent == ply and tbl.AffectPlayerOwner) then
+ oldvel = ent:GetVelocity()
+ phys_ent:SetVelocity(oldvel * (-1 + final_damping) + addvel)
+ ent:SetVelocity(oldvel * (-1 + final_damping) + addvel)
+ end
+ end
+ elseif is_physics then
+ if tbl.PhysicsProps then
+ if not (IsPropProtected(ent, ply) and global_combat_prop_protection:GetBool()) or not unconsenting_owner then
+ if IsValid(phys_ent) then
+ ent:PhysWake()
+ ent:SetVelocity(final_damping * oldvel + addvel)
+ if islocaltorque then
+ phys_ent:SetAngleVelocity(final_damping * phys_ent:GetAngleVelocity())
+ phys_ent:AddAngleVelocity(add_angvel)
+
+ else
+ phys_ent:SetAngleVelocity(final_damping * phys_ent:GetAngleVelocity())
+ add_angvel = phys_ent:WorldToLocalVector( add_angvel )
+ phys_ent:ApplyTorqueCenter(add_angvel)
+ end
+ ent:SetPos(ent:GetPos() + Vector(0,0,0.0001)) --dumb workaround to fight against the ground friction reversing the forces
+ phys_ent:SetVelocity((oldvel * final_damping) + addvel)
+ end
+ end
+ end
+
+ elseif is_npc then
+ if tbl.NPC then
+ if not (IsPropProtected(ent, ply) and global_combat_prop_protection:GetBool()) or not unconsenting_owner then
+ if ent.IsDrGEntity then --welcome to episode 40 of intercompatibility hackery
+ phys_ent = ent.loco
+ local jumpHeight = ent.loco:GetJumpHeight()
+ ent.loco:SetJumpHeight(1)
+ ent.loco:Jump()
+ ent.loco:SetJumpHeight(jumpHeight)
+ end
+ if IsValid(phys_ent) and phys_ent:GetVelocity():Length() > 500 then
+ local vec = oldvel + addvel
+ local clamp_vec = vec:GetNormalized()*500
+ ent:SetVelocity(Vector(0.7 * clamp_vec.x,0.7 * clamp_vec.y,clamp_vec.z)*math.Clamp(1.5*(pos - ent_center):Length()/tbl.Radius,0,1)) --more jank, this one is to prevent some of the weird sliding of npcs by lowering the force as we get closer
+ else
+ ent:SetVelocity((oldvel * final_damping) + addvel)
+ end
+ end
+ end
+
+ elseif tbl.PointEntities or (tbl.AffectSelf and ent == tbl.RootPartOwner) then
+ if not (IsPropProtected(ent, ply) and global_combat_prop_protection:GetBool()) or not unconsenting_owner then
+ phys_ent:SetVelocity(final_damping * oldvel + addvel)
+ end
+ end
+ hook.Run("PhysicsUpdate", ent)
+ hook.Run("PhysicsUpdate", phys_ent)
+ end
+
+ end
+ end
+ --first stage of force: look for targets and determine force amount if continuous
+ local function ImpulseForce(tbl, pos, ang, ply)
+ local ftime = 0.016 --approximate tick duration
+ if tbl.Continuous then
+ tbl.BaseForce = tbl.BaseForce1 * ftime * 3.3333 --weird value to equalize how 600 cancels out gravity
+ tbl.AddedVectorForce = tbl.AddedVectorForce1 * ftime * 3.3333
+ tbl.Torque = tbl.Torque1 * ftime * 3.3333
+ else
+ tbl.BaseForce = tbl.BaseForce1
+ tbl.AddedVectorForce = tbl.AddedVectorForce1
+ tbl.Torque = tbl.Torque1
+ end
+
+ if tbl.HitboxMode == "Sphere" then
+ local ents_hits = ents.FindInSphere(pos, tbl.Radius)
+ ProcessForcesList(ents_hits, tbl, pos, ang, ply)
+ elseif tbl.HitboxMode == "Box" then
+ local mins
+ local maxs
+ if tbl.HitboxMode == "Box" then
+ mins = pos - Vector(tbl.Radius, tbl.Radius, tbl.Length)
+ maxs = pos + Vector(tbl.Radius, tbl.Radius, tbl.Length)
+ end
+
+ local ents_hits = ents.FindInBox(mins, maxs)
+ ProcessForcesList(ents_hits, tbl, pos, ang, ply)
+ elseif tbl.HitboxMode == "Cylinder" then
+ local ents_hits = {}
+ if tbl.Length ~= 0 and tbl.Radius ~= 0 then
+ local counter = 0
+ MergeTargetsByID(ents_hits,ents.FindInSphere(pos, tbl.Radius))
+ for i=0,1,1/(math.abs(tbl.Length/tbl.Radius)) do
+ MergeTargetsByID(ents_hits,ents.FindInSphere(pos + ang:Forward()*tbl.Length*i, tbl.Radius))
+ if counter == 200 then break end
+ counter = counter + 1
+ end
+ MergeTargetsByID(ents_hits,ents.FindInSphere(pos + ang:Forward()*tbl.Length, tbl.Radius))
+ --render.DrawWireframeSphere( self:GetWorldPosition() + self:GetWorldAngles():Forward()*(self.Length - 0.5*self.Radius), 0.5*self.Radius, 10, 10, Color( 255, 255, 255 ) )
+ elseif tbl.Radius == 0 then MergeTargetsByID(ents_hits,ents.FindAlongRay(pos, pos + ang:Forward()*tbl.Length)) end
+ ProcessForcesList(ents_hits, tbl, pos, ang, ply)
+ elseif tbl.HitboxMode == "Cone" then
+ local ents_hits = {}
+ local steps
+ steps = math.Clamp(4*math.ceil(tbl.Length / (tbl.Radius or 1)),1,50)
+ for i = 1,0,-1/steps do
+ MergeTargetsByID(ents_hits,ents.FindInSphere(pos + ang:Forward()*tbl.Length*i, i * tbl.Radius))
+ end
+
+ steps = math.Clamp(math.ceil(tbl.Length / (tbl.Radius or 1)),1,4)
+
+ if tbl.Radius == 0 then MergeTargetsByID(ents_hits,ents.FindAlongRay(pos, pos + ang:Forward()*tbl.Length)) end
+ ProcessForcesList(ents_hits, tbl, pos, ang, ply)
+ elseif tbl.HitboxMode =="Ray" then
+ local startpos = pos + Vector(0,0,0)
+ local endpos = pos + ang:Forward()*tbl.Length
+ ents_hits = ents.FindAlongRay(startpos, endpos)
+ ProcessForcesList(ents_hits, tbl, pos, ang, ply)
+ end
+ end
+
+
+ --consent message from clients
+ net.Receive("pac_signal_player_combat_consent", function(len,ply)
+ local friendly_NPC_preference = net.ReadUInt(2) -- GetConVar("pac_client_npc_exclusion_consent"):GetInt()
+ local grab = net.ReadBool() -- GetConVar("pac_client_grab_consent"):GetBool()
+ local damagezone = net.ReadBool() -- GetConVar("pac_client_damage_zone_consent"):GetBool()
+ local calcview = net.ReadBool() -- GetConVar("pac_client_lock_camera_consent"):GetBool()
+ local force = net.ReadBool() -- GetConVar("pac_client_force_consent"):GetBool()
+ local hitscan = net.ReadBool() -- GetConVar("pac_client_hitscan_consent"):GetBool()
+ friendly_NPC_preferences[ply] = friendly_NPC_preference
+ grab_consents[ply] = grab
+ damage_zone_consents[ply] = damagezone
+ calcview_consents[ply] = calcview
+ force_consents[ply] = force
+ hitscan_consents[ply] = hitscan
+ end)
+
+ --lock break order from client
+ net.Receive("pac_signal_stop_lock", function(len,ply)
+ if not pac.RatelimitPlayer( ply, "pac_signal_stop_lock", 3, 5, {"Player ", ply, " is spamming pac_signal_stop_lock!"} ) then
+ return
+ end
+ ApplyLockState(ply, false)
+ if ply.default_movetype and ply.lock_state_applied and not (ulx and (ply.frozen or ply.jail)) then
+ ply:SetMoveType(ply.default_movetype)
+ targ_ent.default_movetype_reserved = nil
+ end
+ if debugging:GetBool() and can_print[ply] then MsgC(Color(0,255,255), "Requesting lock break!\n") end
+
+ if ply.grabbed_by then --directly go for the grabbed_by player
+ net.Start("pac_request_lock_break")
+ net.WriteEntity(ply)
+ net.WriteString("")
+ net.Send(ply.grabbed_by)
+ end
+ --What if there's more? try to find it AMONG US SUS!
+ for _,ent in pairs(player.GetAll()) do
+ if ent.grabbed_ents and ent ~= ply.grabbed_by then --a player! time to inspect! but skip the already found grabber
+ for _,grabbed in pairs(ent.grabbed_ents) do --check all her entities
+ if ply == grabbed then --that's us!
+ net.Start("pac_request_lock_break")
+ net.WriteEntity(ply)
+ net.WriteString(ply.grabbed_by_uid)
+ net.Send(ent)
+ end
+ end
+ end
+ end
+ end)
+
+ concommand.Add("pac_damage_zone_whitelist_entity_class", function(ply, cmd, args, argStr)
+ if IsValid(ply) then
+ if not ply:IsAdmin() or not pac.RatelimitPlayer( ply, "pac_damage_zone_whitelist_entity_class", 3, 5, {"Player ", ply, " is spamming pac_damage_zone_whitelist_entity_class!"} ) then
+ return
+ end
+ end
+ for _,v in pairs(string.Explode(";",argStr)) do
+ if v ~= "" then
+ damageable_point_ent_classes[v] = true
+ print("added " .. v .. " to the entities you can damage")
+ end
+ end
+ PrintTable(damageable_point_ent_classes)
+ end)
+
+ concommand.Add("pac_damage_zone_blacklist_entity_class", function(ply, cmd, args, argStr)
+ if IsValid(ply) then
+ if not ply:IsAdmin() or not pac.RatelimitPlayer( ply, "pac_damage_zone_blacklist_entity_class", 3, 5, {"Player ", ply, " is spamming pac_damage_zone_blacklist_entity_class!"} ) then
+ return
+ end
+ end
+ for _,v in pairs(string.Explode(";",argStr)) do
+ if v ~= "" then
+ damageable_point_ent_classes[v] = false
+ print("removed " .. v .. " from the entities you can damage")
+ end
+ end
+ PrintTable(damageable_point_ent_classes)
+ end)
+
+
+ util.AddNetworkString("pac_signal_player_combat_consent")
+ util.AddNetworkString("pac_request_blocked_parts")
+ util.AddNetworkString("pac_inform_blocked_parts")
+
+ local FINAL_BLOCKED_COMBAT_FEATURES = {
+ hitscan = false,
+ damage_zone = false,
+ lock = false,
+ force = false,
+ health_modifier = false,
+ }
+
+
+ --[[function net.Incoming( len, client )
+
+ local i = net.ReadHeader()
+ local strName = util.NetworkIDToString( i )
+ if strName ~= "pac_in_editor_posang" and strName ~= "DrGBasePlayerLuminosity" then
+ print(strName, client, "message with " .. len .." bits")
+ end
+
+ if ( !strName ) then return end
+
+ local func = net.Receivers[ strName:lower() ]
+ if ( !func ) then return end
+
+ --
+ -- len includes the 16 bit int which told us the message name
+ --
+ len = len - 16
+
+ func( len, client )
+
+ end]]
+
+ local force_hitbox_ids = {["Box"] = 0,["Cube"] = 1,["Sphere"] = 2,["Cylinder"] = 3,["Cone"] = 4,["Ray"] = 5}
+ local base_force_mode_ids = {["Radial"] = 0, ["Locus"] = 1, ["Local"] = 2}
+ local vect_force_mode_ids = {["Global"] = 0, ["Local"] = 1, ["Radial"] = 2, ["RadialNoPitch"] = 3}
+ local ang_torque_mode_ids = {["Global"] = 0, ["TargetLocal"] = 1, ["Local"] = 2, ["Radial"] = 3}
+ local nextcheckforce = SysTime()
+
+ local function DeclareForceReceivers()
+ util.AddNetworkString("pac_request_force")
+ --the force part impulse request net message
+ net.Receive("pac_request_force", function(len,ply)
+ --server allow
+ if not force_allow:GetBool() then return end
+ if not PlayerIsCombatAllowed(ply) then return end
+
+
+ local tbl = {}
+ local pos = net.ReadVector()
+ if ply:GetPos():DistToSqr(pos) > ENFORCE_DISTANCE_SQR and ENFORCE_DISTANCE_SQR > 0 then return end
+ local ang = net.ReadAngle()
+ tbl.Locus_pos = net.ReadVector()
+ local on = net.ReadBool()
+
+ tbl.UniqueID = net.ReadString()
+
+ if not CountNetMessage(ply) then
+ if debugging:GetBool() and can_print[ply] then MsgC(Color(255,255,0), "[PAC3] Force part: ") MsgC(Color(0,255,255), tostring(ply)) MsgC(Color(200,200,200), " combat actions are too many or too fast! (spam warning)\n") end
+ hook.Remove("Tick", "pac_force_hold"..tbl.UniqueID)
+ active_force_ids[tbl.UniqueID] = nil
+ CountDebugMessage(ply)
+ return
+ end
+
+ tbl.RootPartOwner = net.ReadEntity()
+
+ tbl.HitboxMode = table.KeyFromValue(force_hitbox_ids, net.ReadUInt(4))
+ tbl.BaseForceAngleMode = table.KeyFromValue(base_force_mode_ids, net.ReadUInt(3))
+ tbl.VectorForceAngleMode = table.KeyFromValue(vect_force_mode_ids, net.ReadUInt(2))
+ tbl.TorqueMode = table.KeyFromValue(ang_torque_mode_ids, net.ReadUInt(2))
+
+ tbl.Length = net.ReadInt(16)
+ tbl.Radius = net.ReadInt(16)
+
+ tbl.BaseForce1 = net.ReadInt(18)
+ tbl.AddedVectorForce1 = net.ReadVector()
+ tbl.Torque1 = net.ReadVector()
+
+ tbl.Damping = net.ReadUInt(10)/1000
+ tbl.LevitationHeight = net.ReadInt(14)
+
+ tbl.Continuous = net.ReadBool()
+ tbl.AccountMass = net.ReadBool()
+ tbl.Falloff = net.ReadBool()
+ tbl.ReverseFalloff = net.ReadBool()
+ tbl.DampingFalloff = net.ReadBool()
+ tbl.DampingReverseFalloff = net.ReadBool()
+ tbl.Levitation = net.ReadBool()
+ tbl.AffectSelf = net.ReadBool()
+ tbl.AffectPlayerOwner = net.ReadBool()
+ tbl.Players = net.ReadBool()
+ tbl.PhysicsProps = net.ReadBool()
+ tbl.PointEntities = net.ReadBool()
+ tbl.NPC = net.ReadBool()
+
+ --server limits
+ tbl.Radius = math.Clamp(tbl.Radius,-force_max_radius:GetInt(),force_max_radius:GetInt())
+ tbl.Length = math.Clamp(tbl.Length,-force_max_length:GetInt(),force_max_length:GetInt())
+ tbl.BaseForce = math.Clamp(tbl.BaseForce1,-force_max_amount:GetInt(),force_max_amount:GetInt())
+ tbl.AddedVectorForce1.x = math.Clamp(tbl.AddedVectorForce1.x,-force_max_amount:GetInt(),force_max_amount:GetInt())
+ tbl.AddedVectorForce1.y = math.Clamp(tbl.AddedVectorForce1.y,-force_max_amount:GetInt(),force_max_amount:GetInt())
+ tbl.AddedVectorForce1.z = math.Clamp(tbl.AddedVectorForce1.z,-force_max_amount:GetInt(),force_max_amount:GetInt())
+ tbl.Torque1.x = math.Clamp(tbl.Torque1.x,-force_max_amount:GetInt(),force_max_amount:GetInt())
+ tbl.Torque1.y = math.Clamp(tbl.Torque1.y,-force_max_amount:GetInt(),force_max_amount:GetInt())
+ tbl.Torque1.z = math.Clamp(tbl.Torque1.z,-force_max_amount:GetInt(),force_max_amount:GetInt())
+
+ if on then
+ if tbl.Continuous then
+ hook.Add("Tick", "pac_force_hold"..tbl.UniqueID, function()
+ ImpulseForce(tbl, pos, ang, ply)
+ end)
+
+ active_force_ids[tbl.UniqueID] = CurTime()
+ else
+ active_force_ids[tbl.UniqueID] = nil
+ end
+ ImpulseForce(tbl, pos, ang, ply)
+ else
+ hook.Remove("Tick", "pac_force_hold"..tbl.UniqueID)
+ active_force_ids[tbl.UniqueID] = nil
+ end
+
+ --check bad or inactive hooks
+ for i,v in pairs(active_force_ids) do
+ if not v then
+ hook.Remove("Tick", "pac_force_hold"..i)
+ --print("invalid force")
+ elseif v + 0.1 < CurTime() then
+ hook.Remove("Tick", "pac_force_hold"..i)
+ --print("outdated force")
+ end
+ end
+
+ end)
+
+ hook.Add("Tick", "pac_check_force_hooks", function()
+ if nextcheckforce > SysTime() then return else nextcheckforce = SysTime() + 0.2 end
+ for i,v in pairs(active_force_ids) do
+ if not v then
+ hook.Remove("Tick", "pac_force_hold"..i)
+ --print("removed an invalid force")
+ elseif v + 0.1 < CurTime() then
+ hook.Remove("Tick", "pac_force_hold"..i)
+ --print("removed an outdated force")
+ end
+ end
+
+ end)
+ end
+
+ local active_DoT = {}
+ local requesting_corpses = {}
+
+ local function DeclareDamageZoneReceivers()
+ --networking for damagezone hitparts on corpses
+ hook.Add("CreateEntityRagdoll", "pac_ragdoll_assign", function(ent, rag)
+ if not ent.pac_damagezone_need_send_ragdoll then return end
+ if not ent.pac_damagezone_killer then return end
+ if not damagezone_allow:GetBool() then return end
+ if not damagezone_allow_ragdoll_networking_for_hitpart:GetBool() then return end
+ if not PlayerIsCombatAllowed(ent.pac_damagezone_killer) then return end
+ if not requesting_corpses[ent.pac_damagezone_killer] then return end
+ net.Start("pac_send_ragdoll")
+ net.WriteUInt(ent:EntIndex(), 12)
+ net.WriteEntity(rag)
+ net.Broadcast()
+ end)
+
+ net.Receive("pac_request_ragdoll_sends", function(len, ply)
+ local b = net.ReadBool()
+ if not damagezone_allow:GetBool() then return end
+ if not PlayerIsCombatAllowed(ply) then return end
+ requesting_corpses[ply] = b
+ end)
+
+ util.AddNetworkString("pac_request_zone_damage")
+ util.AddNetworkString("pac_hit_results")
+ util.AddNetworkString("pac_request_ragdoll_sends")
+ util.AddNetworkString("pac_send_ragdoll")
+ net.Receive("pac_request_zone_damage", function(len,ply)
+ --server allow
+ if not damagezone_allow:GetBool() then return end
+ if not PlayerIsCombatAllowed(ply) then return end
+
+ --netrate enforce
+ if not CountNetMessage(ply) then
+ if debugging:GetBool() and can_print[ply] then
+ MsgC(Color(255,255,0), "[PAC3] Damage zone: ") MsgC(Color(0,255,255), tostring(ply)) MsgC(Color(200,200,200), " combat actions are too many or too fast! (spam warning)\n")
+ can_print[ply] = false
+ end
+ CountDebugMessage(ply)
+ return
+ end
+
+ local pos = net.ReadVector()
+ if ply:GetPos():DistToSqr(pos) > ENFORCE_DISTANCE_SQR and ENFORCE_DISTANCE_SQR > 0 then return end
+ local ang = net.ReadAngle()
+ local tbl = {}
+
+ tbl.Damage = net.ReadUInt(28)
+ tbl.MaxHpScaling = net.ReadUInt(10) / 1000
+ tbl.Length = net.ReadInt(16)
+ tbl.Radius = net.ReadInt(16)
+
+ tbl.AffectSelf = net.ReadBool()
+ tbl.NPC = net.ReadBool()
+ tbl.Players = net.ReadBool()
+ tbl.PointEntities = net.ReadBool()
+ tbl.FilterFriendlies = net.ReadBool()
+ tbl.FilterNeutrals = net.ReadBool()
+ tbl.FilterHostiles = net.ReadBool()
+
+ tbl.HitboxMode = table.KeyFromValue(hitbox_ids, net.ReadUInt(5))
+ tbl.DamageType = table.KeyFromValue(damage_ids, net.ReadUInt(7))
+
+ tbl.Detail = net.ReadInt(6)
+ tbl.ExtraSteps = net.ReadInt(4)
+ tbl.RadialRandomize = net.ReadInt(7) / 8
+ tbl.PhaseRandomize = net.ReadInt(7) / 8
+ tbl.DamageFalloff = net.ReadBool()
+ tbl.DamageFalloffPower = net.ReadInt(12) / 8
+ tbl.Bullet = net.ReadBool()
+ tbl.DoNotKill = net.ReadBool()
+ tbl.ReverseDoNotKill = net.ReadBool()
+ tbl.CriticalHealth = net.ReadUInt(16)
+ tbl.RemoveNPCWeaponsOnKill = net.ReadBool()
+
+ tbl.DOTMode = net.ReadBool()
+ tbl.NoInitialDOT = net.ReadBool()
+ tbl.DOTCount = net.ReadUInt(7)
+ tbl.DOTTime = net.ReadUInt(11) / 64
+ tbl.UniqueID = net.ReadString()
+ local do_ents_feedback = net.ReadBool()
+ if not tbl.UniqueID then return end
+
+ if tbl.DOTTime == 0 or (tbl.DOTCount == 0 and not tbl.NoInitialDOT) then
+ tbl.DOTMode = false
+ end
+
+ local dmg_info = DamageInfo()
+
+ --server limits
+ tbl.Radius = math.Clamp(tbl.Radius,-damagezone_max_radius:GetInt(),damagezone_max_radius:GetInt())
+ tbl.Length = math.Clamp(tbl.Length,-damagezone_max_length:GetInt(),damagezone_max_length:GetInt())
+ tbl.Damage = math.Clamp(tbl.Damage,-damagezone_max_damage:GetInt(),damagezone_max_damage:GetInt())
+
+ dmg_info:SetDamage(tbl.Damage)
+ dmg_info:IsBulletDamage(tbl.Bullet)
+ dmg_info:SetDamageForce(Vector(0,0,0))
+ dmg_info:SetAttacker(ply)
+ dmg_info:SetInflictor(ply)
+
+ local ents_hits
+ local kill = false
+ local hit = false
+
+ if damage_types[tbl.DamageType] then
+ if special_damagetypes[tbl.DamageType] then
+ dmg_info:SetDamageType(0)
+ else
+ dmg_info:SetDamageType(damage_types[tbl.DamageType])
+ end
+ else
+ dmg_info:SetDamageType(0)
+ end
+
+ local ratio
+ if tbl.Radius == 0 then ratio = tbl.Length
+ else ratio = math.abs(tbl.Length / tbl.Radius) end
+
+ if tbl.HitboxMode == "Sphere" then
+ ents_hits = ents.FindInSphere(pos, tbl.Radius)
+
+ elseif tbl.HitboxMode == "Box" or tbl.HitboxMode == "Cube" then
+ local mins
+ local maxs
+ if tbl.HitboxMode == "Box" then
+ mins = pos - Vector(tbl.Radius, tbl.Radius, tbl.Length)
+ maxs = pos + Vector(tbl.Radius, tbl.Radius, tbl.Length)
+ elseif tbl.HitboxMode == "Cube" then
+ mins = pos - Vector(tbl.Radius, tbl.Radius, tbl.Radius)
+ maxs = pos + Vector(tbl.Radius, tbl.Radius, tbl.Radius)
+ end
+
+ ents_hits = ents.FindInBox(mins, maxs)
+
+ elseif tbl.HitboxMode == "Cylinder" or tbl.HitboxMode == "CylinderHybrid" then
+ ents_hits = {}
+ if tbl.Radius ~= 0 then
+ local sides = tbl.Detail
+ if tbl.Detail < 1 then sides = 1 end
+ local area_factor = tbl.Radius*tbl.Radius / (400 + 100*tbl.Length/math.max(tbl.Radius,0.1)) --bigger radius means more rays needed to cast to approximate the cylinder detection
+ local steps = 3 + math.ceil(4*(area_factor / ((4 + tbl.Length/4) / (20 / math.max(tbl.Detail,1)))))
+ if tbl.HitboxMode == "CylinderHybrid" and tbl.Length ~= 0 then
+ area_factor = 0.15*area_factor
+ steps = 1 + math.ceil(4*(area_factor / ((4 + tbl.Length/4) / (20 / math.max(tbl.Detail,1)))))
+ end
+ steps = math.max(steps + math.abs(tbl.ExtraSteps),1)
+
+ for ringnumber=1,0,-1/steps do --concentric circles go smaller and smaller by lowering the i multiplier
+ phase = math.random()
+ local ray_thickness = math.Clamp(0.5*math.log(tbl.Radius) + 0.05*tbl.Radius,0,10)*(1 - 0.7*ringnumber)
+ for i=1,0,-1/sides do
+ if ringnumber == 0 then i = 0 end
+ x = ang:Right()*math.cos(2 * math.pi * i + phase * tbl.PhaseRandomize)*tbl.Radius*ringnumber*(1 - math.random() * (ringnumber) * tbl.RadialRandomize)
+ y = ang:Up() *math.sin(2 * math.pi * i + phase * tbl.PhaseRandomize)*tbl.Radius*ringnumber*(1 - math.random() * (ringnumber) * tbl.RadialRandomize)
+ local startpos = pos + x + y
+ local endpos = pos + ang:Forward()*tbl.Length + x + y
+ MergeTargetsByID(ents_hits, ents.FindAlongRay(startpos, endpos, maximized_ray_mins_maxs(startpos,endpos,ray_thickness)))
+ end
+ end
+ if tbl.HitboxMode == "CylinderHybrid" and tbl.Length ~= 0 then
+ --fast sphere check on the wide end
+ if tbl.Length/tbl.Radius >= 2 then
+ MergeTargetsByID(ents_hits,ents.FindInSphere(pos + ang:Forward()*(tbl.Length - tbl.Radius), tbl.Radius))
+ MergeTargetsByID(ents_hits,ents.FindInSphere(pos + ang:Forward()*tbl.Radius, tbl.Radius))
+ if tbl.Radius ~= 0 then
+ local counter = 0
+ for i=math.floor(tbl.Length / tbl.Radius) - 1,1,-1 do
+ MergeTargetsByID(ents_hits,ents.FindInSphere(pos + ang:Forward()*(tbl.Radius*i), tbl.Radius))
+ if counter == 100 then break end
+ counter = counter + 1
+ end
+ end
+ end
+ end
+ elseif tbl.Radius == 0 then MergeTargetsByID(ents_hits,ents.FindAlongRay(pos, pos + ang:Forward()*tbl.Length)) end
+
+ elseif tbl.HitboxMode == "CylinderSpheres" then
+ ents_hits = {}
+ if tbl.Length ~= 0 and tbl.Radius ~= 0 then
+ local counter = 0
+ MergeTargetsByID(ents_hits,ents.FindInSphere(pos, tbl.Radius))
+ for i=0,1,1/(math.abs(tbl.Length/tbl.Radius)) do
+ MergeTargetsByID(ents_hits,ents.FindInSphere(pos + ang:Forward()*tbl.Length*i, tbl.Radius))
+ if counter == 200 then break end
+ counter = counter + 1
+ end
+ MergeTargetsByID(ents_hits,ents.FindInSphere(pos + ang:Forward()*tbl.Length, tbl.Radius))
+ elseif tbl.Radius == 0 then MergeTargetsByID(ents_hits,ents.FindAlongRay(pos, pos + ang:Forward()*tbl.Length)) end
+
+ elseif tbl.HitboxMode == "Cone" or tbl.HitboxMode == "ConeHybrid" then
+ ents_hits = {}
+ if tbl.Radius ~= 0 then
+ local sides = tbl.Detail
+ if tbl.Detail < 1 then sides = 1 end
+ local startpos = pos-- + Vector(0, self.Radius,self.Radius)
+ local area_factor = tbl.Radius*tbl.Radius / (400 + 100*tbl.Length/math.max(tbl.Radius,0.1)) --bigger radius means more rays needed to cast to approximate the cylinder detection
+ local steps = 3 + math.ceil(4*(area_factor / ((4 + tbl.Length/4) / (20 / math.max(tbl.Detail,1)))))
+ if tbl.HitboxMode == "ConeHybrid" and tbl.Length ~= 0 then
+ area_factor = 0.15*area_factor
+ steps = 1 + math.ceil(4*(area_factor / ((4 + tbl.Length/4) / (20 / math.max(tbl.Detail,1)))))
+ end
+ steps = math.max(steps + math.abs(tbl.ExtraSteps),1)
+ local timestart = SysTime()
+ local casts = 0
+ for ringnumber=1,0,-1/steps do --concentric circles go smaller and smaller by lowering the ringnumber multiplier
+ phase = math.random()
+ local ray_thickness = 5 * (2 - ringnumber)
+
+ for i=1,0,-1/sides do
+ if ringnumber == 0 then i = 0 end
+ x = ang:Right()*math.cos(2 * math.pi * i + phase * tbl.PhaseRandomize)*tbl.Radius*ringnumber*(1 - math.random() * (ringnumber) * tbl.RadialRandomize)
+ y = ang:Up() *math.sin(2 * math.pi * i + phase * tbl.PhaseRandomize)*tbl.Radius*ringnumber*(1 - math.random() * (ringnumber) * tbl.RadialRandomize)
+ local endpos = pos + ang:Forward()*tbl.Length + x + y
+ MergeTargetsByID(ents_hits,ents.FindAlongRay(startpos, endpos, maximized_ray_mins_maxs(startpos,endpos,ray_thickness)))
+ casts = casts + 1
+ end
+ end
+ if tbl.HitboxMode == "ConeHybrid" and tbl.Length ~= 0 then
+ --fast sphere check on the wide end
+ local radius_multiplier = math.atan(math.abs(ratio)) / (1.5 + 0.1*math.sqrt(ratio))
+ if ratio > 0.5 then
+ MergeTargetsByID(ents_hits,ents.FindInSphere(pos + ang:Forward()*(tbl.Length - tbl.Radius * radius_multiplier), tbl.Radius * radius_multiplier))
+ end
+ end
+ elseif tbl.Radius == 0 then MergeTargetsByID(ents_hits,ents.FindAlongRay(pos, pos + ang:Forward()*tbl.Length)) end
+
+ elseif tbl.HitboxMode == "ConeSpheres" then
+ ents_hits = {}
+ local steps
+ steps = math.Clamp(4*math.ceil(tbl.Length / (tbl.Radius or 1)),1,50)
+ for i = 1,0,-1/steps do
+ MergeTargetsByID(ents_hits,ents.FindInSphere(pos + ang:Forward()*tbl.Length*i, i * tbl.Radius))
+ end
+
+ steps = math.Clamp(math.ceil(tbl.Length / (tbl.Radius or 1)),1,4)
+
+ if tbl.Radius == 0 then MergeTargetsByID(ents_hits,ents.FindAlongRay(pos, pos + ang:Forward()*tbl.Length)) end
+
+ elseif tbl.HitboxMode =="Ray" then
+ local startpos = pos + Vector(0,0,0)
+ local endpos = pos + ang:Forward()*tbl.Length
+ ents_hits = ents.FindAlongRay(startpos, endpos)
+
+ if tbl.Bullet then
+ local bullet = {}
+ bullet.Src = pos + ang:Forward()
+ bullet.Dir = ang:Forward()*50000
+ bullet.Damage = -1
+ bullet.Force = 0
+ bullet.Entity = dmg_info:GetAttacker()
+ dmg_info:GetInflictor():FireBullets(bullet)
+ end
+ end
+ hit,kill,highest_dmg,successful_hit_ents,successful_kill_ents = ProcessDamagesList(ents_hits, dmg_info, tbl, pos, ang, ply)
+ highest_dmg = highest_dmg or 0
+ net.Start("pac_hit_results", true)
+ net.WriteString(tbl.UniqueID)
+ net.WriteBool(hit)
+ net.WriteBool(kill)
+ net.WriteFloat(highest_dmg)
+ net.WriteBool(hit and do_ents_feedback)
+ if successful_hit_ents and hit and do_ents_feedback and #successful_hit_ents < 20 then
+ local _,bits_before_hit = net.BytesWritten()
+ net.WriteTable(successful_hit_ents, true)
+ local _,bits_after_hit = net.BytesWritten()
+ --print("table is length " .. bits_after_hit - bits_before_hit .. " for " .. table.Count(successful_hit_ents) .. " ents hit, or about " .. ((bits_after_hit - bits_before_hit) / table.Count(successful_hit_ents) - 16) .. " per ent")
+ if kill then net.WriteTable(successful_kill_ents, true) end
+ local _,bits_after_kill = net.BytesWritten()
+ --print("table is length " .. bits_after_kill - bits_after_hit .. " for " .. table.Count(successful_kill_ents) .. " ents killed, or about " .. ((bits_after_kill - bits_after_hit) / table.Count(successful_kill_ents) - 16) .. " per ent")
+ end
+ net.Broadcast()
+ end)
+
+ end
+
+ local nextchecklock = CurTime()
+ local function DeclareLockReceivers()
+ util.AddNetworkString("pac_request_position_override_on_entity_teleport")
+ util.AddNetworkString("pac_request_position_override_on_entity_grab")
+ util.AddNetworkString("pac_request_angle_reset_on_entity")
+ util.AddNetworkString("pac_lock_imposecalcview")
+ util.AddNetworkString("pac_signal_stop_lock")
+ util.AddNetworkString("pac_request_lock_break")
+ util.AddNetworkString("pac_mark_grabbed_ent")
+ util.AddNetworkString("pac_notify_grabbed_player")
+ --The lock part grab request net message
+ net.Receive("pac_request_position_override_on_entity_grab", function(len, ply)
+ --server allow
+ if not lock_allow:GetBool() then return end
+ if not lock_allow_grab:GetBool() then return end
+ if not PlayerIsCombatAllowed(ply) then return end
+
+
+ --netrate enforce
+ if not CountNetMessage(ply) then
+ if debugging:GetBool() and can_print[ply] then
+ MsgC(Color(255,255,0), "[PAC3] Lock grab: ") MsgC(Color(0,255,255), tostring(ply)) MsgC(Color(200,200,200), " combat actions are too many or too fast! (spam warning)\n")
+ can_print[ply] = false
+ end
+ CountDebugMessage(ply)
+ return
+ end
+
+ local did_grab = true
+ local need_breakup = false
+ local breakup_condition = ""
+ --monstrous net message
+ local is_first_time = net.ReadBool()
+ local lockpart_UID = net.ReadString()
+ local pos = net.ReadVector()
+ local ang = net.ReadAngle()
+ local override_ang = net.ReadBool()
+ local override_eyeang = net.ReadBool()
+ local no_collide = net.ReadBool()
+ local targ_ent = net.ReadEntity()
+ local auth_ent = net.ReadEntity()
+ local override_viewposition = net.ReadBool()
+ local alt_pos = net.ReadVector()
+ local alt_ang = net.ReadAngle()
+ local ask_drawviewer = net.ReadBool()
+
+ if targ_ent.CPPICanPhysgun and not targ_ent:CPPICanPhysgun(ply) then return end
+ if ulx and (targ_ent.frozen or targ_ent.jail) then return end --we can't grab frozen/jailed players either
+
+ if ply:GetPos():DistToSqr(pos) > ENFORCE_DISTANCE_SQR and ENFORCE_DISTANCE_SQR > 0 then
+ ApplyLockState(targ_ent, false)
+ if ply.grabbed_ents then
+ net.Start("pac_request_lock_break")
+ net.WriteEntity(targ_ent)
+ net.WriteString(lockpart_UID)
+ net.WriteString("too far!")
+ net.Send(ply)
+ end
+ return
+ end
+
+ local prop_protected, reason = IsPropProtected(targ_ent, ply)
+
+ local owner = Try_CPPIGetOwner(targ_ent) or targ_ent
+ if not IsValid(owner) then return end
+
+
+ local unconsenting_owner = owner ~= ply and (grab_consents[owner] == false or (targ_ent:IsPlayer() and grab_consents[targ_ent] == false))
+ local calcview_unconsenting = owner ~= ply and (calcview_consents[owner] == false or (targ_ent:IsPlayer() and calcview_consents[targ_ent] == false))
+
+ if unconsenting_owner then
+ if owner:IsPlayer() then return
+ elseif (global_combat_prop_protection:GetBool() and prop_protected) then return
+ end
+ end
+
+ local targ_ent_owner = owner or targ_ent
+ local auth_ent_owner = ply
+
+ auth_ent_owner.grabbed_ents = auth_ent_owner.grabbed_ents or {}
+
+ local consent_break_condition = false
+
+ if grab_consents[targ_ent_owner] == false then --if the target player is non-consenting
+ if targ_ent_owner == auth_ent_owner then consent_break_condition = false --player can still grab his owned entities
+ elseif targ_ent:IsPlayer() then --if not the same player, we cannot grab
+ consent_break_condition = true
+ breakup_condition = breakup_condition .. "cannot grab another player if they don't consent to grabs, "
+ elseif global_combat_prop_protection:GetBool() and owner ~= ply then
+ --if entity not owned by grabbing player, he cannot do it to other players' entities in the prop-protected mode
+ consent_break_condition = true
+ breakup_condition = breakup_condition .. "cannot grab another player's owned entities if they don't consent to grabs, "
+ end
+ end
+
+ if not IsValid(targ_ent) then --invalid entity?
+ did_grab = false
+ return --nothing else matters, get out
+ end
+ if consent_break_condition then --any of the non-consenting conditions
+ did_grab = false
+ need_breakup = true
+ breakup_condition = breakup_condition .. "non-consenting, "
+ end
+
+ --dead ent = break
+ --but don't exclude about physics props
+ if targ_ent:Health() == 0 and not (physics_point_ent_classes[targ_ent:GetClass()] or string.find(targ_ent:GetClass(),"item_") or string.find(targ_ent:GetClass(),"ammo_") or targ_ent:IsWeapon()) then
+ did_grab = false
+ need_breakup = true
+ breakup_condition = breakup_condition .. "dead, "
+ end
+
+ if is_first_time then
+ if targ_ent.grabbed_ents then
+ if (auth_ent_owner ~= targ_ent and targ_ent.grabbed_ents[auth_ent_owner] == true) then
+ did_grab = false
+ need_breakup = true
+ breakup_condition = breakup_condition .. "mutual grab prevention, "
+ end
+ end
+
+ end
+
+ if did_grab then
+
+
+ if targ_ent:IsPlayer() and targ_ent:InVehicle() then --yank player out of vehicle
+ if debugging:GetBool() and can_print[ply] then print("Kicking " .. targ_ent:Nick() .. " out of vehicle to be grabbed!") end
+ targ_ent:ExitVehicle()
+ end
+
+ if override_ang then
+ if not targ_ent:IsPlayer() then --non-players work with angles
+ targ_ent:SetAngles(ang)
+ else --players work with eyeangles
+ if override_eyeang then
+
+ if PlayerAllowsCalcView(targ_ent) and override_viewposition then
+ targ_ent.nextcalcviewTick = targ_ent.nextcalcviewTick or CurTime()
+ if targ_ent.nextcalcviewTick < CurTime() then
+ net.Start("pac_lock_imposecalcview")
+ net.WriteBool(true)
+ net.WriteVector(alt_pos)
+ net.WriteAngle(alt_ang)
+ net.WriteBool(ask_drawviewer)
+ net.Send(targ_ent)
+ targ_ent.nextcalcviewTick = CurTime() + 0.1
+ targ_ent.has_calcview = true
+ end
+ targ_ent:SetEyeAngles(alt_ang)
+ targ_ent:SetAngles(alt_ang)
+ else
+ targ_ent:SetEyeAngles(ang)
+ targ_ent:SetAngles(ang)
+ end
+ elseif not override_eyeang or not override_viewposition or not PlayerAllowsCalcView(targ_ent) then --break any calcviews if we can't do that
+ if targ_ent.has_calcview then
+ net.Start("pac_lock_imposecalcview")
+ net.WriteBool(false)
+ net.WriteVector(Vector(0,0,0))
+ net.WriteAngle(Angle(0,0,0))
+ net.Send(targ_ent)
+ targ_ent.has_calcview = false
+ end
+ end
+
+ end
+ end
+
+ targ_ent:SetPos(pos)
+
+ if not targ_ent.lock_state_applied and not targ_ent.default_movetype_reserved then
+ targ_ent.default_movetype = targ_ent:GetMoveType()
+ targ_ent.default_movetype_reserved = true
+ targ_ent.lock_state_applied = true
+ end
+ ApplyLockState(targ_ent, true, no_collide)
+ if targ_ent.IsDrGEntity then
+ targ_ent.loco:SetVelocity(Vector(0,0,0)) --counter gravity speed buildup
+ end
+ if targ_ent:GetClass() == "prop_ragdoll" then targ_ent:GetPhysicsObject():SetPos(pos) end
+
+ --@@note lock assignation! IMPORTANT
+ if is_first_time then --successful, first
+ auth_ent_owner.grabbed_ents[targ_ent] = true
+ targ_ent.grabbed_by = auth_ent_owner
+ targ_ent.grabbed_by_uid = lockpart_UID
+ if debugging:GetBool() and can_print[ply] then print(auth_ent, "grabbed", targ_ent, "owner grabber is", auth_ent_owner) end
+ end
+ targ_ent.grabbed_by_time = CurTime()
+ else
+ auth_ent_owner.grabbed_ents[targ_ent] = nil
+ targ_ent.grabbed_by_uid = nil
+ targ_ent.grabbed_by = nil
+ end
+
+ if need_breakup then
+ if debugging:GetBool() and can_print[ply] then print("stop this now! reason: " .. breakup_condition) end
+ net.Start("pac_request_lock_break")
+ net.WriteEntity(targ_ent)
+ net.WriteString(lockpart_UID)
+ net.WriteString(breakup_condition)
+ net.Send(auth_ent_owner)
+
+ else
+ if is_first_time and did_grab then
+ net.Start("pac_mark_grabbed_ent")
+ net.WriteEntity(targ_ent)
+ net.WriteBool(did_grab)
+ net.WriteString(lockpart_UID)
+ net.Broadcast()
+
+ if targ_ent:IsPlayer() then
+ net.Start("pac_notify_grabbed_player")
+ net.WriteEntity(ply)
+ net.Send(targ_ent)
+ end
+ end
+ end
+ end)
+ --the lockpart teleport request net message
+ net.Receive("pac_request_position_override_on_entity_teleport", function(len, ply)
+ --server allow
+ if not lock_allow:GetBool() then return end
+ if not lock_allow_teleport:GetBool() then return end
+ if not PlayerIsCombatAllowed(ply) then return end
+
+ --netrate enforce
+ if not CountNetMessage(ply) then
+ if debugging:GetBool() and can_print[ply] then
+ MsgC(Color(255,255,0), "[PAC3] Lock teleport: ") MsgC(Color(0,255,255), tostring(ply)) MsgC(Color(200,200,200), " combat actions are too many or too fast! (spam warning)\n")
+ can_print[ply] = false
+ end
+ CountDebugMessage(ply)
+ return
+ end
+
+ local lockpart_UID = net.ReadString()
+ local pos = net.ReadVector()
+ local ang = net.ReadAngle()
+ local override_ang = net.ReadBool()
+
+ if IsValid(ply) then
+ if override_ang then
+ ply:SetEyeAngles(ang)
+ end
+ ply:SetPos(pos)
+ end
+
+ end)
+ --the lockpart grab end request net message
+ net.Receive("pac_request_angle_reset_on_entity", function(len, ply)
+
+ if not PlayerIsCombatAllowed(ply) then return end
+
+ local ang = net.ReadAngle()
+ local delay = net.ReadFloat()
+ local targ_ent = net.ReadEntity()
+ local auth_ent = net.ReadEntity()
+ if targ_ent.CPPICanPhysgun and not targ_ent:CPPICanPhysgun(ply) then return end
+ local prop_protected, reason = IsPropProtected(targ_ent, ply)
+
+ local owner = Try_CPPIGetOwner(targ_ent)
+
+ local unconsenting_owner = owner ~= ply and (grab_consents[owner] == false or (targ_ent:IsPlayer() and grab_consents[targ_ent] == false))
+ if unconsenting_owner then
+ if owner:IsPlayer() then return
+ elseif (global_combat_prop_protection:GetBool() and prop_protected) then return
+ end
+ end
+
+ targ_ent:SetAngles(ang)
+ ApplyLockState(targ_ent, false)
+
+ end)
+
+
+ hook.Add("Tick", "pac_checklocks", function()
+ if nextchecklock > CurTime() then return else nextchecklock = CurTime() + 0.2 end
+ --go through every entity and check if they're still active, if beyond 0.5 seconds we nil out. this is the closest to a regular check
+ for ent,bool in pairs(active_grabbed_ents) do
+ if not IsValid(ent) then
+ active_grabbed_ents[ent] = nil
+ elseif (ent.grabbed_by or bool) then
+ ent.grabbed_by_time = ent.grabbed_by_time or 0
+ if ent.grabbed_by_time + 0.5 < CurTime() then --restore the movetype
+ local grabber = ent.grabbed_by
+ ent.grabbed_by_uid = nil
+ ent.grabbed_by = nil
+ if grabber then
+ grabber.grabbed_ents[ent] = false
+ end
+
+ ApplyLockState(ent, false)
+ active_grabbed_ents[ent] = nil
+ end
+ end
+ end
+ end)
+ end
+
+ local function DeclareHitscanReceivers()
+ util.AddNetworkString("pac_hitscan")
+ net.Receive("pac_hitscan", function(len,ply)
+
+ if not hitscan_allow:GetBool() then return end
+ if not PlayerIsCombatAllowed(ply) then return end
+
+ --netrate enforce
+ if not CountNetMessage(ply) then
+ if debugging:GetBool() and can_print[ply] then
+ MsgC(Color(255,255,0), "[PAC3] Hitscan: ") MsgC(Color(0,255,255), tostring(ply)) MsgC(Color(200,200,200), " combat actions are too many or too fast! (spam warning)\n")
+ can_print[ply] = false
+ end
+ CountDebugMessage(ply)
+ return
+ end
+
+ local bulletinfo = {}
+ local affect_self = net.ReadBool()
+ bulletinfo.Src = net.ReadVector()
+ local dir = net.ReadAngle()
+ bulletinfo.Dir = dir:Forward()
+
+ bulletinfo.dmgtype_str = table.KeyFromValue(damage_ids, net.ReadUInt(7))
+ bulletinfo.dmgtype = damage_types[bulletinfo.dmgtype_str]
+ local spreadx = net.ReadUInt(20) / 10000
+ local spready = net.ReadUInt(20) / 10000
+ bulletinfo.Spread = Vector(spreadx, spready, 0)
+ bulletinfo.Damage = net.ReadUInt(28)
+ bulletinfo.Tracer = net.ReadUInt(8)
+ bulletinfo.Force = net.ReadUInt(16)
+
+ bulletinfo.Distance = net.ReadUInt(16)
+ bulletinfo.Num = net.ReadUInt(9)
+ bulletinfo.TracerName = table.KeyFromValue(tracer_ids, net.ReadUInt(4))
+ bulletinfo.DistributeDamage = net.ReadBool()
+
+ bulletinfo.DamageFalloff = net.ReadBool()
+ bulletinfo.DamageFalloffDistance = net.ReadUInt(16)
+ bulletinfo.DamageFalloffFraction = net.ReadUInt(10) / 1000
+
+ local part_uid = ply:Nick() .. net.ReadString()
+
+ bulletinfo.Num = math.Clamp(bulletinfo.Num, 1, hitscan_max_bullets:GetInt())
+ bulletinfo.Damage = math.Clamp(bulletinfo.Damage, 0, hitscan_max_damage:GetInt())
+ bulletinfo.DamageFalloffFraction = math.Clamp(bulletinfo.DamageFalloffFraction,0,1)
+
+ if hitscan_spreadout_dmg:GetBool() or bulletinfo.DistributeDamage then
+ bulletinfo.Damage = bulletinfo.Damage / bulletinfo.Num
+ end
+
+ if not affect_self then bulletinfo.IgnoreEntity = ply end
+ ply.pac_bullet_emitters = ply.pac_bullet_emitters or {}
+ ply.pac_bullet_emitters[part_uid] = ply.pac_bullet_emitters[part_uid] or ents.Create("pac_bullet_emitter")
+
+ bulletinfo.Attacker = ply
+ bulletinfo.Callback = function(atk, trc, dmg)
+ dmg:SetDamageType(bulletinfo.dmgtype)
+ if trc.Hit and IsValid(trc.Entity) then
+ if not NPCDispositionAllowsIt(ply, trc.Entity) then return {effects = false, damage = false} end
+ local distance = (trc.HitPos):Distance(trc.StartPos)
+ local fraction = math.Clamp(1 - (1-bulletinfo.DamageFalloffFraction)*(distance / bulletinfo.DamageFalloffDistance),bulletinfo.DamageFalloffFraction,1)
+ local ent = trc.Entity
+
+ if bulletinfo.dmgtype_str == "heal" and ent.Health then
+ dmg:SetDamageType(0)
+
+ if ent:Health() < ent:GetMaxHealth() then
+ ent:SetHealth(math.min(ent:Health() + fraction * dmg:GetDamage(), math.max(ent:Health(), ent:GetMaxHealth())))
+ end
+
+ dmg:SetDamage(0)
+ return
+ elseif bulletinfo.dmgtype_str == "armor" and ent.Armor then
+ dmg:SetDamageType(0)
+
+ if ent:Armor() < ent:GetMaxArmor() then
+ ent:SetArmor(math.min(ent:Armor() + fraction * dmg:GetDamage(), math.max(ent:Armor(), ent:GetMaxArmor())))
+ end
+
+ dmg:SetDamage(0)
+ return
+ end
+ if bulletinfo.DamageFalloff and trc.Hit and IsValid(trc.Entity) then
+ if bulletinfo.dmgtype_str ~= "heal" and bulletinfo.dmgtype_str ~= "armor" then
+ dmg:SetDamage(fraction * dmg:GetDamage())
+ end
+ end
+ end
+ end
+
+ if IsValid(ply.pac_bullet_emitters[part_uid]) then
+ ply.pac_bullet_emitters[part_uid]:FireBullets(bulletinfo)
+ else
+ ply.pac_bullet_emitters[part_uid] = ents.Create("pac_bullet_emitter")
+ end
+
+ end)
+ end
+
+ local function DeclareHealthModifierReceivers()
+ util.AddNetworkString("pac_request_healthmod")
+ util.AddNetworkString("pac_update_healthbars")
+ util.AddNetworkString("pac_request_extrahealthbars_action")
+ net.Receive("pac_request_healthmod", function(len,ply)
+ if not healthmod_allow:GetBool() then return end
+
+ --netrate enforce
+ if not CountNetMessage(ply) then
+ if debugging:GetBool() and can_print[ply] then
+ MsgC(Color(255,255,0), "[PAC3] Health modifier: ") MsgC(Color(0,255,255), tostring(ply)) MsgC(Color(200,200,200), " combat actions are too many or too fast! (spam warning)\n")
+ can_print[ply] = false
+ end
+ CountDebugMessage(ply)
+ return
+ end
+
+ local part_uid = net.ReadString()
+ local mod_id = net.ReadString()
+ local action = net.ReadString()
+
+ if action == "MaxHealth" then
+ if not healthmod_allow:GetBool() then return end
+ local num = net.ReadUInt(32)
+ num = math.Clamp(num,0,healthmod_max_value:GetInt())
+ local follow = net.ReadBool()
+ if not healthmod_allow_change_maxhp:GetBool() then return end
+ if ply:Health() == ply:GetMaxHealth() and follow then
+ ply:SetHealth(num)
+ elseif num < ply:Health() then
+ ply:SetHealth(num)
+ end
+ ply:SetMaxHealth(num)
+ ply.pac_healthmods = ply.pac_healthmods or {}
+ ply.pac_healthmods[part_uid] = ply.pac_healthmods[part_uid] or {}
+ ply.pac_healthmods[part_uid].maxhealth = num
+
+ elseif action == "MaxArmor" then
+ if not healthmod_allow:GetBool() then return end
+ local num = net.ReadUInt(32)
+ num = math.Clamp(num,0,healthmod_max_value:GetInt())
+ local follow = net.ReadBool()
+ if not healthmod_allow_change_maxhp:GetBool() then return end
+ if ply:Armor() == ply:GetMaxArmor() and follow then
+ ply:SetArmor(num)
+ elseif num < ply:Armor() then
+ ply:SetArmor(num)
+ end
+ ply:SetMaxArmor(num)
+ ply.pac_healthmods = ply.pac_healthmods or {}
+ ply.pac_healthmods[part_uid] = ply.pac_healthmods[part_uid] or {}
+ ply.pac_healthmods[part_uid].maxarmor = num
+
+ elseif action == "DamageMultiplier" then
+ local scale = net.ReadFloat()
+ AddDamageScale(ply, mod_id, scale, part_uid)
+
+ elseif action == "HealthBars" then
+ if not healthmod_allowed_extra_bars:GetBool() then return end
+ local num = net.ReadUInt(32)
+ local barsize = net.ReadUInt(32)
+ local layer = net.ReadUInt(4)
+ local absorbfactor = net.ReadFloat()
+ local follow = net.ReadBool()
+ local counted_hits = net.ReadBool()
+ local no_overflow = net.ReadBool()
+
+ if counted_hits and not healthmod_allowed_counted_hits:GetBool() then return end
+
+ local requested_amount = num * barsize
+
+ local current_bars_amount_without_this = GatherExtraHPBars(ply, part_uid)
+ local allowed_amount_without_this = healthmod_max_extra_bars_value:GetInt() - current_bars_amount_without_this
+
+ if requested_amount >= allowed_amount_without_this then
+ requested_amount = math.Clamp(requested_amount,0,allowed_amount_without_this)
+
+ barsize = math.floor(requested_amount / num)
+ num = math.floor(requested_amount / barsize)
+
+ UpdateHealthBars(ply, num, barsize, layer, absorbfactor, part_uid, follow, counted_hits, no_overflow)
+ else
+ UpdateHealthBars(ply, num, barsize, layer, absorbfactor, part_uid, follow, counted_hits, no_overflow)
+ end
+
+ elseif action == "OnRemove" then
+ if ply.pac_damage_scalings then
+ if ply.pac_damage_scalings[part_uid] then
+ ply.pac_damage_scalings[part_uid] = nil
+ end
+ end
+ if ply.pac_healthmods then
+ ply.pac_healthmods[part_uid] = nil
+ end
+
+ FixMaxHealths(ply)
+ UpdateHealthBars(ply, 0, 0, 0, 0, part_uid, follow)
+ end
+ SendUpdateHealthBars(ply)
+ end)
+ net.Receive("pac_request_extrahealthbars_action", function(len, ply)
+ local part_uid = net.ReadString()
+ local action = net.ReadString()
+ local num = net.ReadInt(16)
+ UpdateHealthBarsFromCMD(ply, action, num, part_uid)
+ SendUpdateHealthBars(ply)
+ end)
+ end
+
+ --[[util.AddNetworkString("pac_hitscan")
+ util.AddNetworkString("pac_request_position_override_on_entity_teleport")
+ util.AddNetworkString("pac_request_position_override_on_entity_grab")
+ util.AddNetworkString("pac_request_angle_reset_on_entity")
+ util.AddNetworkString("pac_request_zone_damage")
+ util.AddNetworkString("pac_hit_results")
+ util.AddNetworkString("pac_request_force")
+
+ util.AddNetworkString("pac_signal_stop_lock")
+ util.AddNetworkString("pac_request_lock_break")
+ util.AddNetworkString("pac_lock_imposecalcview")
+ util.AddNetworkString("pac_mark_grabbed_ent")
+ util.AddNetworkString("pac_notify_grabbed_player")
+ util.AddNetworkString("pac_request_healthmod")
+ util.AddNetworkString("pac_update_healthbars")]]
+
+ if master_init_featureblocker:GetInt() == 0 then
+ FINAL_BLOCKED_COMBAT_FEATURES = {
+ hitscan = false,
+ damage_zone = false,
+ lock = false,
+ force = false,
+ health_modifier = false,
+ }
+
+
+ elseif master_init_featureblocker:GetInt() == 1 then
+ FINAL_BLOCKED_COMBAT_FEATURES = {
+ hitscan = not hitscan_allow:GetBool(),
+ damage_zone = not damagezone_allow:GetBool(),
+ lock = not lock_allow:GetBool(),
+ force = not force_allow:GetBool(),
+ health_modifier = not healthmod_allow:GetBool(),
+ }
+
+ else -- if it's not 0 or 1, all net combat features will be removed!
+ FINAL_BLOCKED_COMBAT_FEATURES = {
+ hitscan = true,
+ damage_zone = true,
+ lock = true,
+ force = true,
+ health_modifier = true,
+ }
+ end
+
+ if not FINAL_BLOCKED_COMBAT_FEATURES["force"] then DeclareForceReceivers() end
+ if not FINAL_BLOCKED_COMBAT_FEATURES["damage_zone"] then DeclareDamageZoneReceivers() end
+ if not FINAL_BLOCKED_COMBAT_FEATURES["lock"] then DeclareLockReceivers() end
+ if not FINAL_BLOCKED_COMBAT_FEATURES["hitscan"] then DeclareHitscanReceivers() end
+ if not FINAL_BLOCKED_COMBAT_FEATURES["health_modifier"] then DeclareHealthModifierReceivers() end
+
+ local function ReinitializeCombatReceivers()
+ for name,blocked in pairs(FINAL_BLOCKED_COMBAT_FEATURES) do
+ local update = blocked and (blocked == GetConVar("pac_sv_"..name):GetBool())
+ local new_bool = not (blocked or not GetConVar("pac_sv_"..name):GetBool())
+
+ if update then
+ FINAL_BLOCKED_COMBAT_FEATURES[name] = new_bool
+ if name == "force" then DeclareForceReceivers() print("reinitialized " .. name)
+ elseif name == "damage_zone" then DeclareDamageZoneReceivers() print("reinitialized " .. name)
+ elseif name == "lock" then DeclareLockReceivers() print("reinitialized " .. name)
+ elseif name == "hitscan" then DeclareHitscanReceivers() print("reinitialized " .. name)
+ elseif name == "health_modifier" then DeclareHealthModifierReceivers() print("reinitialized " .. name)
+ end
+ end
+ end
+ net.Start("pac_inform_blocked_parts")
+ net.WriteTable(FINAL_BLOCKED_COMBAT_FEATURES)
+ net.Broadcast()
+ end
+
+ concommand.Add("pac_sv_combat_reinitialize_missing_receivers", function(ply)
+ if IsValid(ply) then
+ if not ply:IsAdmin() or not pac.RatelimitPlayer( ply, "pac_sv_combat_reinitialize_missing_receivers", 3, 5, {"Player ", ply, " is spamming pac_sv_combat_reinitialize_missing_receivers!"} ) then
+ return
+ end
+ ReinitializeCombatReceivers()
+ end
+
+ end)
+
+ util.AddNetworkString("pac_request_blocked_parts_reinitialization")
+ net.Receive("pac_request_blocked_parts_reinitialization", function(len, ply)
+ if IsValid(ply) then
+ if not ply:IsAdmin() or not pac.RatelimitPlayer( ply, "pac_sv_combat_reinitialize_missing_receivers", 3, 5, {"Player ", ply, " is spamming pac_sv_combat_reinitialize_missing_receivers!"} ) then
+ return
+ end
+ ReinitializeCombatReceivers()
+ end
+ end)
+
+ net.Receive("pac_request_blocked_parts", function(len, ply)
+ net.Start("pac_inform_blocked_parts")
+ net.WriteTable(FINAL_BLOCKED_COMBAT_FEATURES)
+ net.Send(ply)
+ end)
+
+end
+
+if CLIENT then
+ killicon.Add( "pac_bullet_emitter", "icon16/user_gray.png", Color(255,255,255) )
+
+ concommand.Add("pac_sv_reinitialize_missing_combat_parts_remotely", function(ply)
+ if IsValid(ply) then
+ if not ply:IsAdmin() then
+ return
+ end
+ net.Start("pac_request_blocked_parts_reinitialization")
+ net.SendToServer()
+ end
+ end)
+
+
+ CreateConVar("pac_client_npc_exclusion_consent", "0", {FCVAR_ARCHIVE}, "Whether you want to protect some npcs based on their disposition or faction. So far it only works with Dispositions.0 = ignore factions and relationships and target any NPC\n1 = protect friendlies\n2 = protect friendlies and neutrals")
+ CreateConVar("pac_client_grab_consent", "0", {FCVAR_ARCHIVE}, "Whether you want to consent to being grabbed by other players in PAC3 with the lock part")
+ CreateConVar("pac_client_lock_camera_consent", "0", {FCVAR_ARCHIVE}, "Whether you want to consent to having lock parts override your view")
+ CreateConVar("pac_client_damage_zone_consent", "0", {FCVAR_ARCHIVE}, "Whether you want to consent to receiving damage by other players in PAC3 with the damage zone part")
+ CreateConVar("pac_client_force_consent", "0", {FCVAR_ARCHIVE}, "Whether you want to consent to pac3 physics forces")
+ CreateConVar("pac_client_hitscan_consent", "0", {FCVAR_ARCHIVE}, "Whether you want to consent to receiving damage by other players in PAC3 with the hitscan part.")
+
+ function pac.CountNetMessage()
+ local ply = LocalPlayer()
+
+ local stime = SysTime()
+ local ms_basis = GetConVar("pac_sv_combat_enforce_netrate"):GetInt()/1000
+ local base_allowance = GetConVar("pac_sv_combat_enforce_netrate_buffersize"):GetInt()
+
+ ply.pac_netmessage_allowance = ply.pac_netmessage_allowance or base_allowance
+ ply.pac_netmessage_allowance_time = ply.pac_netmessage_allowance_time or 0 --initialize fields
+
+ local timedelta = stime - ply.pac_netmessage_allowance_time --in seconds
+ ply.pac_netmessage_allowance_time = stime
+ local regen_rate = math.Clamp(ms_basis,0.01,10) / 20 --delay (converted from milliseconds) -> frequency (1/seconds)
+ local regens = timedelta / regen_rate
+ --print(timedelta .. " s, " .. 1/regen_rate .. "/s, " .. regens .. " regens")
+ if base_allowance == 0 then --limiting only by time, with no reserves
+ return timedelta > ms_basis
+ elseif ms_basis == 0 then --allowance with 0 time means ??? I guess automatic pass
+ return true
+ else
+ if timedelta > ms_basis then --good, count up
+ --print("good time: +"..regens .. "->" .. math.Clamp(ply.pac_netmessage_allowance + math.min(regens,base_allowance), -1, base_allowance))
+ ply.pac_netmessage_allowance = math.Clamp(ply.pac_netmessage_allowance + math.min(regens,base_allowance), -1, base_allowance)
+ else --earlier than base delay, so count down the allowance
+ --print("bad time: -1")
+ ply.pac_netmessage_allowance = ply.pac_netmessage_allowance - 1
+ end
+ ply.pac_netmessage_allowance = math.Clamp(ply.pac_netmessage_allowance,-1,base_allowance)
+ ply.pac_netmessage_allowance_time = stime
+ return ply.pac_netmessage_allowance ~= -1
+ end
+
+ end
+
+ local function SendConsents()
+ net.Start("pac_signal_player_combat_consent")
+ net.WriteUInt(GetConVar("pac_client_npc_exclusion_consent"):GetInt(),2)
+ net.WriteBool(GetConVar("pac_client_grab_consent"):GetBool())
+ net.WriteBool(GetConVar("pac_client_damage_zone_consent"):GetBool())
+ net.WriteBool(GetConVar("pac_client_lock_camera_consent"):GetBool())
+ net.WriteBool(GetConVar("pac_client_force_consent"):GetBool())
+ net.WriteBool(GetConVar("pac_client_hitscan_consent"):GetBool())
+ net.SendToServer()
+ end
+
+
+ local function RequestBlockedParts()
+ net.Start("pac_request_blocked_parts")
+ net.SendToServer()
+ end
+
+ concommand.Add("pac_inform_about_blocked_parts", function()
+ RequestBlockedParts()
+ pac.Message("Manually fetching info about pac3 combat parts...")
+
+ timer.Simple(2, function()
+ for name,b in pairs(pac.Blocked_Combat_Parts) do
+ local blocked = b
+ local disabled = not GetConVar("pac_sv_"..name):GetBool()
+
+ local bool_str
+
+ if disabled and blocked then bool_str = "disabled and blocked -> unavailable"
+ elseif disabled and not blocked then bool_str = "disabled -> unavailable"
+ elseif not disabled and blocked then bool_str = "blocked - > unavailable"
+ elseif not disabled and not blocked then bool_str = "available"
+ else bool_str = "??" end
+
+ print(name .. " is " .. bool_str)
+ end
+ end)
+ end)
+
+ net.Receive("pac_inform_blocked_parts", function() --silent
+ pac.Blocked_Combat_Parts = net.ReadTable()
+ end)
+
+ local consent_cvars = {"pac_client_npc_exclusion_consent", "pac_client_grab_consent", "pac_client_lock_camera_consent", "pac_client_damage_zone_consent", "pac_client_force_consent", "pac_client_hitscan_consent"}
+ for _,cmd in ipairs(consent_cvars) do
+ cvars.AddChangeCallback(cmd, SendConsents)
+ end
+
+ CreateConVar("pac_break_lock_verbosity", "3", FCVAR_ARCHIVE, "How much info you want for the PAC3 lock notifications\n3:full information\n2:grabbing player + basic reminder of the lock break command\n1:grabbing player\n0:suppress the notifications")
+
+
+ concommand.Add( "pac_stop_lock", function()
+ net.Start("pac_signal_stop_lock")
+ net.SendToServer()
+ end, nil, "asks the server to breakup any lockpart hold on your player")
+
+ concommand.Add( "pac_break_lock", function()
+ net.Start("pac_signal_stop_lock")
+ net.SendToServer()
+ end, nil, "asks the server to breakup any lockpart hold on your player")
+
+ net.Receive("pac_lock_imposecalcview", function()
+ local authority_to_calcview = net.ReadBool() and GetConVar("pac_client_lock_camera_consent"):GetBool()
+
+ local alt_pos = net.ReadVector()
+ local alt_ang = net.ReadAngle()
+ local ask_drawviewer = net.ReadBool()
+
+ if authority_to_calcview then
+ LocalPlayer().last_calcview = CurTime()
+ LocalPlayer().has_calcview = true
+ hook.Add("CalcView", "PAC_lockpart_calcview", function(ply, pos, angles, fov)
+ if LocalPlayer().last_calcview + 0.5 < CurTime() then
+ hook.Remove("CalcView", "PAC_lockpart_calcview")
+ LocalPlayer().has_calcview = false
+ return nil
+
+ end
+ local view = {
+ origin = alt_pos,
+ angles = alt_ang,
+ fov = fov,
+ drawviewer = ask_drawviewer
+ }
+ return view
+ end)
+ hook.Add("Tick", "pac_checkcalcview", function()
+ if LocalPlayer().has_calcview and LocalPlayer().last_calcview + 0.5 < CurTime() then
+ hook.Remove("CalcView", "PAC_lockpart_calcview")
+ LocalPlayer().has_calcview = false
+ --print("killed a calcview due to expiry")
+ end
+ if LocalPlayer().last_calcview + 0.5 < CurTime() then
+ hook.Remove("CalcView", "PAC_lockpart_calcview")
+ LocalPlayer().has_calcview = false
+ --print("killed a calcview again due to expiry")
+ end
+ end)
+ else --if LocalPlayer().has_calcview then
+ hook.Remove("CalcView", "PAC_lockpart_calcview")
+ --print("killed a calcview due to lack of authority")
+ end
+ end)
+
+ net.Receive("pac_request_player_combat_consent_update", function()
+ SendConsents()
+ end)
+
+ net.Receive("pac_notify_grabbed_player", function()
+ local grabber = net.ReadEntity()
+ local verbosity = GetConVar("pac_break_lock_verbosity"):GetInt()
+ local str
+ if verbosity == 3 then
+ str = "[PAC3] You've been grabbed by " .. grabber:Nick() .. "! You can break free with pac_break_lock or pac_stop_lock. You can suppress these messages with pac_break_lock_verbosity 0"
+ notification.AddLegacy( str, NOTIFY_HINT, 10 )
+ elseif verbosity == 2 then
+ str = "[PAC3] You've been grabbed by " .. grabber:Nick() .. "! pac_break_lock to break free"
+ notification.AddLegacy( str, NOTIFY_HINT, 7 )
+ elseif verbosity == 1 then
+ str = "[PAC3] You've been grabbed by " .. grabber:Nick() .. "!"
+ notification.AddLegacy( str, NOTIFY_HINT, 7 )
+ end
+
+ pac.Message("You've been grabbed by " .. grabber:Nick() .. "!")
+
+ end)
+
+ hook.Add("InitPostEntity", "PAC_Send_Consents_On_Join", SendConsents)
+ hook.Add("InitPostEntity", "PAC_Request_BlockedParts_On_Join", RequestBlockedParts)
+ pac.Blocked_Combat_Parts = pac.Blocked_Combat_Parts or {}
+end
diff --git a/lua/pac3/extra/shared/projectiles.lua b/lua/pac3/extra/shared/projectiles.lua
index 7189ad6f1..cfcf0f14b 100644
--- a/lua/pac3/extra/shared/projectiles.lua
+++ b/lua/pac3/extra/shared/projectiles.lua
@@ -1,6 +1,11 @@
-local enable = CreateConVar("pac_sv_projectiles", 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED})
-local pac_sv_projectile_max_attract_radius = CreateConVar("pac_sv_projectile_max_attract_radius", 300, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED})
-local pac_sv_projectile_max_damage_radius = CreateConVar("pac_sv_projectile_max_damage_radius", 100, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED})
+local enable = CreateConVar("pac_sv_projectiles", 0, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "allow physical projectiles serverside")
+local pac_sv_projectile_max_attract_radius = CreateConVar("pac_sv_projectile_max_attract_radius", 300, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "maximum attract radius for physical projectiles")
+local pac_sv_projectile_max_damage_radius = CreateConVar("pac_sv_projectile_max_damage_radius", 100, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "maximum damage radius for physical projectiles")
+local pac_sv_projectile_max_phys_radius = CreateConVar("pac_sv_projectile_max_phys_radius", 100, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "maximum physical radius for physical projectiles")
+local pac_sv_projectile_max_speed = CreateConVar("pac_sv_projectile_max_speed", 100, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "maximum speed for physical projectiles")
+local pac_sv_projectile_max_damage = CreateConVar("pac_sv_projectile_max_damage", 100000, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "maximum damage for physical projectiles")
+local pac_sv_projectile_max_mass = CreateConVar("pac_sv_projectile_max_mass", 50000, CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "maximum speed for physical projectiles")
+local pac_sv_projectile_allow_custom_collision_mesh = CreateConVar("pac_sv_projectile_allow_custom_collision_mesh", "1", CLIENT and {FCVAR_REPLICATED} or {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "Whether to allow other models' collision mesh as a physical projectile, rather than just box and sphere")
do -- projectile entity
local ENT = {}
@@ -39,6 +44,13 @@ do -- projectile entity
end
if SERVER then
+ local physprop_indices = {}
+ for i=0,200,1 do
+ local name = util.GetSurfacePropName(i)
+ if name ~= "" then
+ physprop_indices[name] = i
+ end
+ end
pac.AddHook("EntityTakeDamage", "pac_projectile", function(ent, dmg)
local a, i = dmg:GetAttacker(), dmg:GetInflictor()
@@ -60,17 +72,50 @@ do -- projectile entity
self.projectile_owner = ply
- local radius = math.Clamp(part.Radius, 1, pac_sv_projectile_max_damage_radius:GetFloat())
-
+ local radius = math.Clamp(part.Radius, 0.01, pac_sv_projectile_max_phys_radius:GetFloat())
if part.Sphere then
- self:PhysicsInitSphere(radius)
+ self:PhysicsInitSphere(radius, part.SurfaceProperties)
else
- self:PhysicsInitBox(Vector(1,1,1) * - radius, Vector(1,1,1) * radius)
+ local valid_fallback = util.IsValidModel( part.FallbackSurfpropModel ) and not IsUselessModel(part.FallbackSurfpropModel) and pac_sv_projectile_allow_custom_collision_mesh:GetBool()
+ --print("valid fallback? " .. part.FallbackSurfpropModel , valid_fallback)
+ self:PhysicsInitBox(Vector(1,1,1) * - radius, Vector(1,1,1) * radius, part.SurfaceProperties)
+
+ if part.OverridePhysMesh and valid_fallback then
+ self:SetModel(part.FallbackSurfpropModel)
+ self:PhysicsInit(SOLID_VPHYSICS)
+ end
+
+ if valid_fallback and part.RescalePhysMesh then
+ local physmesh = self:GetPhysicsObject():GetMeshConvexes()
+ --hack from prop resizer
+ for convexkey, convex in ipairs( physmesh ) do
+ for poskey, postab in ipairs( convex ) do
+ convex[ poskey ] = postab.pos * radius
+ end
+ end
+
+ self:PhysicsInitMultiConvex( physmesh, part.SurfaceProperties)
+ self:EnableCustomCollisions( true )
+ elseif not valid_fallback then
+ self:PhysicsInitBox(Vector(1,1,1) * - radius, Vector(1,1,1) * radius, part.SurfaceProperties)
+ end
+
end
+
local phys = self:GetPhysicsObject()
+ phys:SetMaterial(part.SurfaceProperties)
+
phys:EnableGravity(part.Gravity)
- phys:AddVelocity((ang:Forward() + (VectorRand():Angle():Forward() * part.Spread)) * part.Speed * 1000)
+ if not part.Freeze then
+ phys:AddVelocity((ang:Forward() + (VectorRand():Angle():Forward() * part.Spread)) * part.Speed * 1000)
+ phys:AddAngleVelocity(Vector(part.RandomAngleVelocity.x * math.Rand(-1,1), part.RandomAngleVelocity.y * math.Rand(-1,1), part.RandomAngleVelocity.z * math.Rand(-1,1)))
+ else
+ phys:EnableMotion(false)
+ end
+
+ phys:AddAngleVelocity(part.LocalAngleVelocity)
+
if part.AddOwnerSpeed then
phys:AddVelocity(ply:GetVelocity())
end
@@ -85,12 +130,15 @@ do -- projectile entity
phys:EnableCollisions(false)
end
- phys:SetMass(math.Clamp(part.Mass, 0.001, 50000))
+
+ phys:SetMass(math.Clamp(part.Mass, 0.001, pac_sv_projectile_max_mass:GetFloat()))
phys:SetDamping(0, 0)
+ self.phys = phys
self:SetAimDir(part.AimDir)
-
+ self:DrawShadow(part.DrawShadow)
self.part_data = part
+ self.surface_data = util.GetSurfaceData(physprop_indices[part.SurfaceProperties])
end
local damage_types = {
@@ -238,6 +286,20 @@ do -- projectile entity
if not self.part_data then return end
if not self.projectile_owner:IsValid() then return end
+ local our_surfdata = self.surface_data
+ local their_surfdata = util.GetSurfaceData(data.TheirSurfaceProps)
+
+ if (self.part_data.ImpactSounds) then
+ if data.Speed >= 300 then
+ if (data.Speed >= our_surfdata.hardVelocityThreshold) or (our_surfdata.hardnessFactor >= their_surfdata.hardThreshold) then
+ self:EmitSound(our_surfdata.impactHardSound)
+ else
+ self:EmitSound(our_surfdata.impactSoftSound)
+ end
+ elseif data.Speed >= 50 then
+ self:EmitSound(our_surfdata.impactSoftSound)
+ end
+ end
net.Start("pac_projectile_collide_event", true)
net.WriteEntity(self)
net.WriteTable({}) -- nothing for now
@@ -314,13 +376,13 @@ do -- projectile entity
end
end
- local damage_radius = math.Clamp(self.part_data.DamageRadius, 0, 300)
+ local damage_radius = math.Clamp(self.part_data.DamageRadius, 0, pac_sv_projectile_max_damage_radius:GetFloat())
if self.part_data.Damage > 0 then
if self.part_data.DamageType == "heal" then
if damage_radius > 0 then
for _, ent in ipairs(ents.FindInSphere(data.HitPos, damage_radius)) do
- if ent ~= ply or self.part_data.CollideWithOwner then
+ if (ent ~= ply or self.part_data.CollideWithOwner) and ent:Health() < ent:GetMaxHealth() then
ent:SetHealth(math.min(ent:Health() + self.part_data.Damage, ent:GetMaxHealth()))
end
end
@@ -331,8 +393,9 @@ do -- projectile entity
if damage_radius > 0 then
for _, ent in ipairs(ents.FindInSphere(data.HitPos, damage_radius)) do
if ent.SetArmor and ent.Armor then
- if ent ~= ply or self.part_data.CollideWithOwner then
- ent:SetArmor(math.min(ent:Armor() + self.part_data.Damage, ent.GetMaxArmor and ent:GetMaxArmor() or 100))
+ local maxArmor = ent.GetMaxArmor and ent:GetMaxArmor() or 100
+ if (ent ~= ply or self.part_data.CollideWithOwner) and ent:Armor() < maxArmor then
+ ent:SetArmor(math.min(ent:Armor() + self.part_data.Damage, maxArmor))
end
end
end
@@ -359,7 +422,7 @@ do -- projectile entity
end
elseif self.part_data.DamageType == "explosion" then
info:SetDamageType(damage_types.blast)
- info:SetDamage(math.Clamp(self.part_data.Damage, 0, 100000))
+ info:SetDamage(math.Clamp(self.part_data.Damage, 0, pac_sv_projectile_max_damage:GetFloat()))
util.BlastDamageInfo(info, data.HitPos, damage_radius)
else
info:SetDamageForce(data.OurOldVelocity)
@@ -399,6 +462,59 @@ do -- projectile entity
scripted_ents.Register(ENT, ENT.ClassName)
end
+
+local damage_ids = {
+ generic = 0, --generic damage
+ crush = 1, --caused by physics interaction
+ bullet = 2, --bullet damage
+ slash = 3, --sharp objects, such as manhacks or other npcs attacks
+ burn = 4, --damage from fire
+ vehicle = 5, --hit by a vehicle
+ fall = 6, --fall damage
+ blast = 7, --explosion damage
+ club = 8, --crowbar damage
+ shock = 9, --electrical damage, shows smoke at the damage position
+ sonic = 10, --sonic damage,used by the gargantua and houndeye npcs
+ energybeam = 11, --laser
+ nevergib = 12, --don't create gibs
+ alwaysgib = 13, --always create gibs
+ drown = 14, --drown damage
+ paralyze = 15, --same as dmg_poison
+ nervegas = 16, --neurotoxin damage
+ poison = 17, --poison damage
+ acid = 18, --
+ airboat = 19, --airboat gun damage
+ blast_surface = 20, --this won't hurt the player underwater
+ buckshot = 21, --the pellets fired from a shotgun
+ direct = 22, --
+ dissolve = 23, --forces the entity to dissolve on death
+ drownrecover = 24, --damage applied to the player to restore health after drowning
+ physgun = 25, --damage done by the gravity gun
+ plasma = 26, --
+ prevent_physics_force = 27, --
+ radiation = 28, --radiation
+ removenoragdoll = 29, --don't create a ragdoll on death
+ slowburn = 30, --
+
+ explosion = 31, -- ent:Ignite(5)
+ fire = 32, -- ent:Ignite(5)
+
+ -- env_entity_dissolver
+ dissolve_energy = 33,
+ dissolve_heavy_electrical = 34,
+ dissolve_light_electrical = 35,
+ dissolve_core_effect = 36,
+
+ heal = 37,
+ armor = 38,
+}
+local attract_ids = {
+ hitpos = 0,
+ hitpos_radius = 1,
+ closest_to_projectile = 2,
+ closest_to_hitpos = 3,
+}
+
if SERVER then
for key, ent in pairs(ents.FindByClass("pac_projectile")) do
ent:Remove()
@@ -406,20 +522,70 @@ if SERVER then
util.AddNetworkString("pac_projectile")
util.AddNetworkString("pac_projectile_attach")
+ util.AddNetworkString("pac_projectile_remove")
+ --REWORKED NET MESSAGE STRUCTURE MEANS THERE'S A LIMITED AMOUNT OF RECEIVED TABLE FIELDS
net.Receive("pac_projectile", function(len, ply)
if not enable:GetBool() then return end
pace.suppress_prop_spawn = true
- if hook.Run("PlayerSpawnProp", ply, "models/props_junk/popcan01a.mdl") == false then
+ if hook.Run("PlayerSpawnProp", ply, "models/props_junk/PopCan01a.mdl") == false then
pace.suppress_prop_spawn = nil
return
end
pace.suppress_prop_spawn = nil
+ local multi_projectile_count = net.ReadUInt(7)
local pos = net.ReadVector()
local ang = net.ReadAngle()
- local part = net.ReadTable()
+ local part = {}
+
+ --bools
+ part.Sphere = net.ReadBool()
+ part.RemoveOnCollide = net.ReadBool()
+ part.CollideWithOwner = net.ReadBool()
+ part.RemoveOnHide = net.ReadBool()
+ part.RescalePhysMesh = net.ReadBool()
+ part.OverridePhysMesh = net.ReadBool()
+ part.Gravity = net.ReadBool()
+ part.AddOwnerSpeed = net.ReadBool()
+ part.Collisions = net.ReadBool()
+ part.CollideWithSelf = net.ReadBool()
+ part.AimDir = net.ReadBool()
+ part.DrawShadow = net.ReadBool()
+ part.Sticky = net.ReadBool()
+ part.BulletImpact = net.ReadBool()
+ part.Freeze = net.ReadBool()
+ part.ImpactSounds = net.ReadBool()
+
+ --vectors
+ part.RandomAngleVelocity = net.ReadVector()
+ part.LocalAngleVelocity = net.ReadVector()
+
+ --strings
+ part.FallbackSurfpropModel = "models/" .. net.ReadString()
+
+ part.UniqueID = net.ReadString()
+ part.SurfaceProperties = util.GetSurfacePropName(net.ReadUInt(10))
+ part.DamageType = table.KeyFromValue(damage_ids, net.ReadUInt(7))
+ part.AttractMode = table.KeyFromValue(attract_ids, net.ReadUInt(3))
+
+ --numbers
+ local using_decimal = net.ReadBool()
+ if not using_decimal then part.Radius = net.ReadUInt(12) else part.Radius = net.ReadFloat() end
+
+ part.DamageRadius = net.ReadUInt(12)
+ part.Damage = math.Clamp(net.ReadUInt(24), 0, pac_sv_projectile_max_damage:GetFloat())
+ part.Speed = math.Clamp(net.ReadInt(18) / 1000, -pac_sv_projectile_max_speed:GetFloat(), pac_sv_projectile_max_speed:GetFloat())
+ part.Maximum = net.ReadUInt(7)
+ part.LifeTime = net.ReadUInt(14) / 100
+ part.Delay = net.ReadUInt(13) / 100
+ part.Mass = net.ReadUInt(16)
+ part.Spread = net.ReadInt(10) / 100
+ part.Damping = net.ReadInt(20) / 100
+ part.Attract = net.ReadInt(14)
+ part.AttractRadius = net.ReadUInt(10)
+ part.Bounce = net.ReadInt(15) / 100
local radius_limit = 2000
@@ -453,7 +619,7 @@ if SERVER then
end
if projectile_count > 50 then
- pac.Message("Player ", ply, " has more than 50 projectiles spawned!")
+ pac.Message("Player ", ply, " has more than 50 projectiles spawned! No more will be spawned until some expire.")
return
end
@@ -464,11 +630,15 @@ if SERVER then
local ent = ents.Create("pac_projectile")
SafeRemoveEntityDelayed(ent,math.Clamp(part.LifeTime, 0, 50))
- ent:SetModel("models/props_junk/popcan01a.mdl")
+ local valid_fallback = util.IsValidModel( part.FallbackSurfpropModel ) and not IsUselessModel(part.FallbackSurfpropModel)
+ if not valid_fallback or part.FallbackSurfpropModel == "models/" or not part.OverridePhysMesh then part.FallbackSurfpropModel = "models/props_junk/PopCan01a.mdl" end
+
+ ent:SetModel(part.FallbackSurfpropModel)
ent:SetPos(pos)
ent:SetAngles(ang)
ent:Spawn()
+
if not part.CollideWithOwner then
ent:SetOwner(ply)
end
@@ -485,6 +655,7 @@ if SERVER then
net.WriteEntity(ply)
net.WriteInt(ent:EntIndex(), 16)
net.WriteString(part.UniqueID)
+ net.WriteString(part.SurfaceProperties)
net.Broadcast()
ent.pac_projectile_uid = part.UniqueID
@@ -494,10 +665,68 @@ if SERVER then
ent.pac_projectile_owner = ply
end
- if part.Delay == 0 then
- spawn()
+ local function multispawn()
+ if not ply:IsValid() then return end
+
+ ply.pac_projectiles = ply.pac_projectiles or {}
+
+ local projectile_count = 0
+ for ent in pairs(ply.pac_projectiles) do
+ if ent:IsValid() then
+ projectile_count = projectile_count + 1
+ else
+ ply.pac_projectiles[ent] = nil
+ end
+ end
+
+ local remaining_projectile_slots = math.max(50 - projectile_count,0)
+
+ if (multi_projectile_count > remaining_projectile_slots) then
+ if remaining_projectile_slots == 0 then
+ --block the spawns
+ pac.Message("Player ", ply, " has 50 projectiles spawned! No more will be spawned until some expire.")
+ goto CONTINUE
+ else
+ --adjust the spawn to just the limit
+ pac.Message("Player ", ply, " will spawn only ",remaining_projectile_slots," projectiles to prevent going over-limit")
+ multi_projectile_count = remaining_projectile_slots
+ end
+
+ end
+ if part.Maximum > 0 and projectile_count >= part.Maximum then
+ return
+ end
+
+ for i = multi_projectile_count - 1, 0, -1 do
+ spawn()
+ end
+
+ ::CONTINUE::
+ end
+
+ if multi_projectile_count == 1 then
+ if part.Delay == 0 then
+ spawn()
+ else
+ timer.Simple(part.Delay, spawn)
+ end
else
- timer.Simple(part.Delay, spawn)
+ if part.Delay == 0 then
+ multispawn()
+ else
+ timer.Simple(part.Delay, multispawn)
+ end
+ end
+ end)
+
+ net.Receive("pac_projectile_remove", function()
+ local id = net.ReadInt(16)
+ local ent = ents.GetByIndex(id)
+
+ if ent.part_data.RemoveOnHide then
+ SafeRemoveEntity(ent)
end
+
end)
+
end
diff --git a/lua/pac3/libraries/shader_params.lua b/lua/pac3/libraries/shader_params.lua
index 685a94df2..2bec13b9f 100644
--- a/lua/pac3/libraries/shader_params.lua
+++ b/lua/pac3/libraries/shader_params.lua
@@ -159,6 +159,13 @@ return {
}
},
vertexlitgeneric = {
+ ["environment map"] = {
+ envmapfresnel = {
+ type = "float",
+ friendly = "Fresnel",
+ description = "like $fresnelreflection. requires phong.",
+ },
+ },
wrinkle = {
compress = {
type = "texture",
@@ -624,6 +631,12 @@ return {
default = false,
description = "flag",
},
+ allowdiffusemodulation = {
+ type = "bool",
+ default = true,
+ friendly = "AllowDiffuseModulation",
+ description = "Prevents the material from being tinted.",
+ }
},
["bump map"] = {
bumpmap = {
diff --git a/lua/pac3/libraries/webaudio.lua b/lua/pac3/libraries/webaudio.lua
index 3dc9f1d0c..bbb50f773 100644
--- a/lua/pac3/libraries/webaudio.lua
+++ b/lua/pac3/libraries/webaudio.lua
@@ -783,6 +783,8 @@ do
end
function META:SetVolume(volumeFraction)
+ pac.volume = pac.volume or 1
+ volumeFraction = volumeFraction * pac.volume
if self.Volume == volumeFraction then return self end
self.Volume = volumeFraction
diff --git a/materials/icon64/new pac icon.png b/materials/icon64/new pac icon.png
new file mode 100644
index 000000000..b7088ceab
Binary files /dev/null and b/materials/icon64/new pac icon.png differ
diff --git a/models/pac/circle.dx80.vtx b/models/pac/circle.dx80.vtx
new file mode 100644
index 000000000..3619c0ca8
Binary files /dev/null and b/models/pac/circle.dx80.vtx differ
diff --git a/models/pac/circle.dx90.vtx b/models/pac/circle.dx90.vtx
new file mode 100644
index 000000000..892ea62bb
Binary files /dev/null and b/models/pac/circle.dx90.vtx differ
diff --git a/models/pac/circle.mdl b/models/pac/circle.mdl
new file mode 100644
index 000000000..352ae80e6
Binary files /dev/null and b/models/pac/circle.mdl differ
diff --git a/models/pac/circle.sw.vtx b/models/pac/circle.sw.vtx
new file mode 100644
index 000000000..0ef65cbe6
Binary files /dev/null and b/models/pac/circle.sw.vtx differ
diff --git a/models/pac/circle.vvd b/models/pac/circle.vvd
new file mode 100644
index 000000000..9a028eb6f
Binary files /dev/null and b/models/pac/circle.vvd differ
diff --git a/models/pac/plane.dx80.vtx b/models/pac/plane.dx80.vtx
new file mode 100644
index 000000000..05795dbd6
Binary files /dev/null and b/models/pac/plane.dx80.vtx differ
diff --git a/models/pac/plane.dx90.vtx b/models/pac/plane.dx90.vtx
new file mode 100644
index 000000000..cc63afd75
Binary files /dev/null and b/models/pac/plane.dx90.vtx differ
diff --git a/models/pac/plane.mdl b/models/pac/plane.mdl
new file mode 100644
index 000000000..4bcc940b6
Binary files /dev/null and b/models/pac/plane.mdl differ
diff --git a/models/pac/plane.sw.vtx b/models/pac/plane.sw.vtx
new file mode 100644
index 000000000..6e23c3822
Binary files /dev/null and b/models/pac/plane.sw.vtx differ
diff --git a/models/pac/plane.vvd b/models/pac/plane.vvd
new file mode 100644
index 000000000..4ac2e1768
Binary files /dev/null and b/models/pac/plane.vvd differ