Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
4d600ee
fix animation editor playback button not resetting on "gesture"
Techbot121 Sep 20, 2026
2e4c177
display animation time in the tree instead of the weird total frames
Techbot121 Sep 20, 2026
97dc72d
add custom_animation_time event
Techbot121 Sep 20, 2026
dcf98c7
better event names for the custom animation event and events in general
Techbot121 Sep 20, 2026
9358ac3
more color tools for lighting
pingu7867 Sep 21, 2026
25ad9a9
other ui fix
pingu7867 Sep 21, 2026
b72595b
show a message when the editor is hidden or passthrough
Techbot121 Sep 21, 2026
4fefc3b
animation timeline improvements
Techbot121 Sep 21, 2026
ddd215c
try to guard against empty outfits saving over your current one
Techbot121 Sep 21, 2026
a82681d
fix outfit decode
pingu7867 Sep 22, 2026
0de2ebf
Update saved_parts.lua
pingu7867 Sep 22, 2026
cce8845
animated particles and brownian motion
pingu7867 Sep 26, 2026
642ce9e
dont use gmod beta functions
pingu7867 Sep 26, 2026
6a17444
radial symmetry
Advers Sep 27, 2026
2a8b2bb
animated particles patch
pingu7867 Sep 29, 2026
66fd319
asset browser patch
pingu7867 Oct 1, 2026
4c23582
fixes
pingu7867 Oct 2, 2026
4928759
Add files via upload
lionstrykes10911 Oct 4, 2026
db9fab6
Update size.lua
lionstrykes10911 Oct 5, 2026
3c4a6b3
Update entity.lua
lionstrykes10911 Oct 5, 2026
13dc78a
Update model.lua
lionstrykes10911 Oct 5, 2026
0ac94d2
Update size.lua
lionstrykes10911 Oct 5, 2026
59bb023
Update size.lua
lionstrykes10911 Oct 5, 2026
f24b8af
Update size.lua
lionstrykes10911 Oct 5, 2026
069ec14
Merge pull request #1474 from Advers/radial
pingu7867 Oct 9, 2026
8be1e1a
step/view backward compatibility
pingu7867 Oct 10, 2026
9519bd6
nil check
pingu7867 Oct 10, 2026
80b3a41
turn that light off when you leave.
lionstrykes10911 Oct 10, 2026
041d155
partial optimization and correction
pingu7867 Oct 10, 2026
1e10423
add hull-like step/view visualization
pingu7867 Oct 11, 2026
e7ef592
Merge pull request #1476 from lionstrykes10911/develop
pingu7867 Oct 11, 2026
ca42f09
fix: only apply storable part properties from network data
James159758 Oct 11, 2026
5a0ecb9
Merge pull request #1477 from James159758/fix/part-settable-whitelist
Techbot121 Oct 11, 2026
3166a00
Merge remote-tracking branch 'upstream/develop' into develop
JustMrPhoenix Oct 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions lua/pac3/core/client/base_part.lua
Original file line number Diff line number Diff line change
Expand Up @@ -1147,13 +1147,17 @@ do -- serializing
for key, value in next, tbl.self do
if key == "UniqueID" then goto CONTINUE end

if self["Set" .. key] then
-- security: only apply declared (storable) properties. Internal
-- setters (SetPlayerOwner, SetShowInEditor, SetOwner, ...) must
-- never be reachable from network-provided part data.
-- Legit outfits always contain StorableVars only (see ToTable).
if self.StorableVars[key] and self["Set" .. key] then
if key == "Material" then
table_insert(self.delayed_variables, {key = key, val = value})
end
self["Set" .. key](self, value)
elseif key ~= "ClassName" then
pac.dprint("settable: unhandled key [%q] = %q", key, tostring(value))
pac.dprint("settable: ignored non-storable key [%q] = %q", key, tostring(value))
end

::CONTINUE::
Expand Down
15 changes: 14 additions & 1 deletion lua/pac3/core/client/bones.lua
Original file line number Diff line number Diff line change
Expand Up @@ -407,7 +407,7 @@ do -- bone manipulation for boneanimlib

if pac_boneanim then
for i = 0, count do
ManipulateBoneScale(ent, i, SCALE_RESET)
ManipulateBoneScale(ent, i, (pac_boneanim.scales and pac_boneanim.scales[i]) or SCALE_RESET)
ManipulateBonePosition(ent, i, pac_boneanim.positions[i] or ORIGIN_RESET)
ManipulateBoneAngles(ent, i, pac_boneanim.angles[i] or ANGLE_RESET)
ManipulateBoneJiggle(ent, i, 0)
Expand Down Expand Up @@ -452,12 +452,14 @@ do -- bone manipulation for boneanimlib
function pac.SetEntityBoneMatrix(ent, i, matrix)
pac.ManipulateBonePosition(ent, i, matrix:GetTranslation())
pac.ManipulateBoneAngles(ent, i, matrix:GetAngles())
pac.ManipulateBoneScale(ent, i, matrix:GetScale())
end


function pac.ResetEntityBoneMatrix(ent, i)
pac.ManipulateBoneAngles(ent, i, angle_zero)
pac.ManipulateBonePosition(ent, i, vector_origin)
pac.ManipulateBoneScale(ent, i, SCALE_RESET)
end

function pac.ManipulateBonePosition(ply, id, var)
Expand All @@ -479,6 +481,17 @@ do -- bone manipulation for boneanimlib
ply:ManipulateBoneAngles(id, var)
end
end

function pac.ManipulateBoneScale(ply, id, var)
ply.pac_boneanim = ply.pac_boneanim or {positions = {}, angles = {}}
ply.pac_boneanim.scales = ply.pac_boneanim.scales or {}

ply.pac_boneanim.scales[id] = var

if not ply.pac_has_parts then
ply:ManipulateBoneScale(id, var)
end
end
end

if Entity(1):IsPlayer() and not PAC_RESTART then
Expand Down
207 changes: 190 additions & 17 deletions lua/pac3/core/client/parts/event.lua
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@ local Vector = Vector
local util = util
local SysTime = SysTime

-- localization helper
local function L(str)
if pace and pace.LanguageString then
return pace.LanguageString(str)
end
return str
end


local BUILDER, PART = pac.PartTemplate("base")

Expand Down Expand Up @@ -452,7 +460,7 @@ function PART:GetDynamicProperties(reset_to_default)
if udata.default then
arg.set(udata.default)
else
arg.set(nil)
arg.set(get_default(typ))
end
end
end
Expand Down Expand Up @@ -3337,18 +3345,31 @@ do
return self.extra_nice_name(part, ent, part:GetParsedArgumentsForObject(self))
end

local str = part:GetEvent()
local str = L(pac.PrettifyName(part:GetEvent()))

if part:GetArguments() ~= "" then
local args = part:GetArguments():gsub(";", " or ")
local args = {part:GetParsedArgumentsForObject(self)}
local labeled = {}
for pos, argData in ipairs(self:GetArguments()) do
local val = args[pos]
if val ~= nil then
if isstring(val) then
val = val:gsub(";", " or ")
end
labeled[#labeled + 1] = L(pac.PrettifyName(argData[1])) .. " " .. tostring(val)
end
end

if not tonumber(args) then
args = [["]] .. args .. [["]]
if #labeled > 0 then
local operator = part:GetOperator()
if operator ~= "" then
str = str .. " " .. L(operator)
end
str = str .. " " .. table.concat(labeled, ", ")
end
str = str .. " " .. part:GetOperator() .. " " .. args
end

return pac.PrettifyName(str)
return str
end

local eventMetaTable = {
Expand Down Expand Up @@ -3454,19 +3475,57 @@ do
end)
end

-- helpers shared by the custom animation events
local function get_registered_animation_data(anim_id)
return pac.animations.GetRegisteredAnimations()[anim_id]
end

local function get_animation_duration(anim_id)
local data = get_registered_animation_data(anim_id)
if not data or not data.FrameData then return 0 end

local total = 0
for i = 1, #data.FrameData do
total = total + (1 / (data.FrameData[i].FrameRate or 1))
end
return total
end

local function get_animation_frame_count(anim_id)
local data = get_registered_animation_data(anim_id)
if not data or not data.FrameData then return 1 end
return #data.FrameData
end

-- custom animation event
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
nice = function(self, ent, animation, frame_start, frame_end)
if animation == "" then self:SetWarning("no animation selected") return L("no animation") end
local part = pac.GetLocalPart(animation)
if not IsValid(part) then self:SetError("invalid animation selected") return "invalid animation" end
if not IsValid(part) then self:SetError("invalid animation selected") return L("invalid animation selected") end
self:SetWarning()
return part:GetName()

local label = part:GetName()
local data = get_registered_animation_data(part:GetAnimID())
local total = data and #data.FrameData or "--"

local start = frame_start or 1
if start < 1 then start = 1 end
local endf = frame_end and frame_end > 0 and frame_end or total
local window = start .. "-" .. tostring(endf)

if ent:IsValid() then
local frame, delta = animations.GetEntityAnimationFrame(ent, part:GetAnimID())
if frame and delta then
return label .. " [" .. L("frame") .. " " .. frame .. "/" .. total .. " | " .. window .. "]"
end
end
return label .. " [" .. L("frame") .. " --/" .. total .. " | " .. window .. "]"
end,
args = {
{"animation", "string", {editor_panel = "custom_animation_frame"}},
Expand All @@ -3475,9 +3534,8 @@ do
local anim = pace.current_part:GetProperty("animation")
if anim ~= "" then
local part = pac.GetLocalPart(anim)
-- GetAnimationDuration only works while editor is active for some reason
local data = util.JSONToTable(part:GetData())
return math.Clamp(math.ceil(num), 1, #data.FrameData)
if not IsValid(part) then return end
return math.Clamp(math.ceil(num), 1, get_animation_frame_count(part:GetAnimID()))
end
end
}},
Expand All @@ -3487,9 +3545,8 @@ do
local start = pace.current_part:GetProperty("frame_start")
if anim ~= "" then
local part = pac.GetLocalPart(anim)
-- GetAnimationDuration only works while editor is active for some reason
local data = util.JSONToTable(part:GetData())
return math.Clamp(math.ceil(num), start, #data.FrameData)
if not IsValid(part) then return end
return math.Clamp(math.ceil(num), start, get_animation_frame_count(part:GetAnimID()))
end
end
}},
Expand All @@ -3511,6 +3568,11 @@ do
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
frame_start = math.max(frame_start, 1) -- frames are 1-based
-- frame_end stays 0 unless touched, so treat 0 as "until the animation ends"
if frame_end <= 0 then
frame_end = #animations.GetEntityAnimation(ent, part:GetAnimID()).FrameData
end
return frame >= frame_start and frame <= frame_end
end
end
Expand All @@ -3534,6 +3596,117 @@ do
pac.RegisterEvent(eventObject)
end

-- custom animation time event
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's playback time is somewhere between the seconds specified,\nbased on the same time readout as the animation editor's timeline",
name = "custom_animation_time",
nice = function(self, ent, animation, time_start, time_end)
if animation == "" then self:SetWarning("no animation selected") return L("no animation") end
local part = pac.GetLocalPart(animation)
if not IsValid(part) then self:SetError("invalid animation selected") return L("invalid animation selected") end
self:SetWarning()

local label = part:GetName()
local duration = get_animation_duration(part:GetAnimID())

local t_start = time_start or 0
if t_start < 0 then t_start = 0 end
local t_end
if time_end and time_end > t_start then
t_end = time_end
elseif duration > 0 then
t_end = duration
else
t_end = time_end or 0
end
local window = string.format("%.2f-%.2f%s", t_start, t_end, L("s"))

if ent:IsValid() then
local frame, delta = animations.GetEntityAnimationFrame(ent, part:GetAnimID())
if frame and delta then
local cycle = animations.GetEntityAnimationCycle(ent, part:GetAnimID())
local duration_live = animations.GetAnimationDuration(ent, part:GetAnimID())
local time = cycle ~= nil and duration_live and (cycle * duration_live) or 0
return label .. " [" .. string.format("%.2f%s", time, L("s")) .. " | " .. window .. "]"
end
end
return label .. " [--.--" .. L("s") .. " | " .. window .. "]"
end,
args = {
{"animation", "string", {editor_panel = "custom_animation_frame"}},
{"time_start", "number", {
editor_onchange = function(self, num)
local anim = pace.current_part:GetProperty("animation")
if anim ~= "" then
local part = pac.GetLocalPart(anim)
if not IsValid(part) then return end
return math.Clamp(num, 0, get_animation_duration(part:GetAnimID()))
end
end
}},
{"time_end", "number", {
editor_onchange = function(self, num)
local anim = pace.current_part:GetProperty("animation")
local start = pace.current_part:GetProperty("time_start")
if anim ~= "" then
local part = pac.GetLocalPart(anim)
if not IsValid(part) then return end
return math.Clamp(num, start, get_animation_duration(part:GetAnimID()))
end
end
}}
},
available = function(self, eventPart)
return next(animations.registered) and true or false
end,
func = function (self, eventPart, ent, animation, time_start, time_end)
local time_start = time_start or 0
local time_end = time_end or 0
if not animation or animation == "" then return end
if not IsValid(ent) then return end
if not next(animations.playing) then return end
for i,v in ipairs(animations.playing) 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
local cycle = animations.GetEntityAnimationCycle(ent, part:GetAnimID())
if not cycle then return end
local duration = animations.GetAnimationDuration(ent, part:GetAnimID())
-- time_end stays 0 unless touched, so treat any end that is not
-- after the start as "until the animation ends"
if time_end <= time_start then
time_end = duration
end
local time = cycle * duration
return time >= time_start and time <= time_end
end
end
end
}

local eventObject = pac.CreateEvent(event.name, event.args)
eventObject.Think = event.func
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

-- DarkRP default events
do
local plyMeta = FindMetaTable('Player')
Expand Down
5 changes: 3 additions & 2 deletions lua/pac3/core/client/parts/model.lua
Original file line number Diff line number Diff line change
Expand Up @@ -800,10 +800,11 @@ function PART:ApplyMatrix()
StandingHullHeight = self.StandingHullHeight,
CrouchingHullHeight = self.CrouchingHullHeight,
HullWidth = self.HullWidth,
OverrideStepSize = self.OverrideStepSize,
OverrideView = self.OverrideView,
StepSize = self.StepSize,
StandingViewOffset = self.StandingViewOffset,
CrouchingViewOffset = self.CrouchingViewOffset,
OverrideStepAndView = self.OverrideStepAndView
CrouchingViewOffset = self.CrouchingViewOffset
})

if self.Size == 1 and self.Scale == vec_one then
Expand Down
Loading
Loading