├── README.md ├── TUTORIAL.md ├── addon.json ├── docs ├── DESIGN.md └── smh_architecture.png ├── lua ├── autorun │ ├── client │ │ └── derma_utils.lua │ ├── smh.lua │ └── translations.lua └── smh │ ├── client.lua │ ├── client │ ├── concommands.lua │ ├── controller.lua │ ├── derma │ │ ├── frame_panel.lua │ │ ├── frame_pointer.lua │ │ ├── load.lua │ │ ├── physrecord.lua │ │ ├── properties.lua │ │ ├── save.lua │ │ ├── settings.lua │ │ ├── smh_menu.lua │ │ ├── spawn.lua │ │ └── world_clicker.lua │ ├── highlighter.lua │ ├── physrecord.lua │ ├── renderer.lua │ ├── settings.lua │ ├── state.lua │ └── ui.lua │ ├── modifiers │ ├── advcamera.lua │ ├── advlights.lua │ ├── bodygroup.lua │ ├── bones.lua │ ├── color.lua │ ├── eyetarget.lua │ ├── flex.lua │ ├── modelscale.lua │ ├── physbones.lua │ ├── poseparameter.lua │ ├── position.lua │ ├── skin.lua │ └── softlamps.lua │ ├── server.lua │ ├── server │ ├── controller.lua │ ├── easing.lua │ ├── eyetarget.lua │ ├── ghosts_manager.lua │ ├── keyframe_data.lua │ ├── keyframe_manager.lua │ ├── modifiers.lua │ ├── physrecord.lua │ ├── playback_manager.lua │ ├── properties_manager.lua │ ├── spawn_manager.lua │ └── worldkeyframes_manager.lua │ ├── shared.lua │ └── shared │ ├── saves.lua │ └── tablesplit.lua └── workshop_export.sh /README.md: -------------------------------------------------------------------------------- 1 | Stop Motion Helper 2 | ================== 3 | Stop Motion Helper is a tool for Garry's Mod designed to make stop motion animation easier and more manageable. 4 | It can also be used for recorded animation, but it is mainly designed around stop motion. 5 | 6 | If you have any bug fixes or improvements you have made, feel free to make a pull request. However, before you do, please make sure your code works, is clean and clear (identation, variable names that are understandable, and comments for not-so-understandable parts) and uses reactive paradigm like the rest of the addon. -------------------------------------------------------------------------------- /TUTORIAL.md: -------------------------------------------------------------------------------- 1 | Stop Motion Helper tutorial 2 | =========================== 3 | 4 | ## Video tutorial 5 | 6 | You can find a video tutorial covering the basics [here](https://www.youtube.com/embed/9RBynRzBdhk). 7 | 8 | ## How things basically work 9 | 10 | Before we dive into details, let's go through how the tool basically works. When you open the SMH menu, you'll notice 11 | the white vertical lines going in a row from left to right. This is the timeline. The white lines are representing frames. 12 | The white rectangle with a black outline is the playhead, which can be used to cycle through our frames. 13 | 14 | When you select an entity and drag the playhead around, nothing happens. This is because the timeline has no 15 | recorded frames. Recorded frames are where a recorded state of the entity is stored. They are represented as 16 | green rectangles in the timeline, and they can be moved, copied and removed. When the playhead is moved on 17 | top of a recorded frame, the recorded state will be applied to the entity, and when the playhead is between two 18 | recorded frames, the entity's state will be tweened between them. 19 | 20 | ## Opening the menu 21 | 22 | The menu is opened using the `+smh_menu` console command. If you don't know how to access the console, 23 | there are plenty of tutorials available showing you how to do so, so google up! Now, bind a key to the command, using 24 | `bind +smh_menu`. The menu will open when you hold down your bound key. 25 | 26 | Before we can record frames, we need to tell SMH what entity we want to animate. An entity can be selected by right clicking 27 | objects on the screen while the SMH menu is open. When an entity is glowing with a green outline, that entity is currently selected. 28 | 29 | **NOTE:** The selected entity is the entity that you want to edit frames for. All entities that have any recorded frames are animated, so you don't need to select all of the entities that you want to animate seperately. 30 | 31 | ## Moving the playhead 32 | 33 | You can simply left click and hold the playhead rectangle to drag it on the timeline. Left clicking in any empty space in the timeline will move the playhead to the nearest frame. 34 | You can also bind keys to `smh_next` and `smh_previous` commands, so you won't have to use the menu all the time. 35 | 36 | ## Recording frames 37 | 38 | Move the playhead to the position in the timeline where you want to record your frame. Then simply press the record button on the right. 39 | You can also bind a key to smh_record command, which has the same function as the record button, for easier access. 40 | 41 | ## Managing recorded frames 42 | 43 | After a recorded frame has been created, it is shown as a green rectangle in the timeline. You can click and hold the rectangle with your left 44 | mouse button to move the frame to another position. You can remove the frame by right clicking the rectangle. You can click and hold it with 45 | your middle mouse button to make a copy of the frame to another position. Copying can also be done by holding down Ctrl and right clicking. 46 | 47 | You can select multiple keyframes by selecting them with left click and Ctrl one by one, or with left click and Shift on 2 keyframes will 48 | select those keyframes and all keyframes between them. All selected can be unselected by left clicking on any keyframe. 49 | 50 | **NOTE:** Moving a frame on top of another frame will remove the frame that is not being moved. Be careful. 51 | 52 | ## Adding or reducing frames 53 | 54 | By default, the timeline is 100 frames long. This can be changed from the "Frame count" input. Frame count determines the amount of frames 55 | visible on the timeline as well as playback and rendering, which we will discuss below. 56 | 57 | **NOTE:** If recorded frames go outside your frame count, they are not removed. They just won't be visible in the timeline. 58 | 59 | ## Scrolling and zooming on frame timeline 60 | 61 | There is a scroll bar at the bottom of the frame timeline. Drag it to scroll through frames that are outside of the current view. 62 | If you want to see less or more frames in the timeline at once, you can zoom in and out by using your mouse wheel. 63 | 64 | ## Properties 65 | 66 | Properties menu allows you to name and select already recorded entities and manage timelines for the selected entity. 67 | 68 | For editing timelines, you can add up to 10 timelines for an entity, and select up to 13 modifiers between those timelines that could be 69 | manipulated by SMH. 70 | 71 | Modifiers: 72 | 73 | `Nonphysical Bones` — Model bones that can not be manipulated by the Physics gun, like fingers. 74 | 75 | `Color` — Color from the Color tool. 76 | 77 | `Bodygroup` — Bodygroups that can be usually changed through Context Menu. 78 | 79 | `Model scale` 80 | 81 | `Soft Lamps` — Properties of lamp entities from Soft Lamps addon. 82 | 83 | `Pose parameters` — Animates pose parameters, those can be edited with the Easy Animation Tool addon. 84 | 85 | `Eye target` — Eyes that can be manipulated with Eye Poser. 86 | 87 | `Skin` — Skins that can be usually changed through Context Menu. 88 | 89 | `Facial flexes` — Facial flexes that are manipulated with the Faceposer. 90 | 91 | `Advanced Cameras` — Properties of cameras from Advanced Cameras addon. 92 | 93 | `Physical Bones` — Anything that obeys physics and can be manipulated by Physics Gun. 94 | 95 | `Position and Rotation` — Records position of the entity, however it doesn't seem to do anything for ragdolls, and on physics 96 | props its position will be overriden by `Physical Bones` modifier. However, it is still recommended to record this modifier on them. 97 | 98 | `Advanced Lights` — Properties of light entities from Advanced Light Entities addon. 99 | 100 | **NOTE:** Recording new entity will create 1 timeline with all modifiers enabled on it, but if you want to use specific timeline setup, you 101 | can use `smh_savepreset` console command to save timeline setup on your selected entity to use for newly recorded ones. You can 102 | access settings files by navigating to `garrysmod/data/smhsettings`. 103 | 104 | ## World keyframes 105 | 106 | Pressing "Select World" button in the Properties menu to select world, on which you can record special keyframes that you can edit in the 107 | Properies window. Those keyframes can execute console commands, which can be entered just as you would enter them in console, and those 108 | keyframes also can trigger specific gmod entities that can be activated through keypresses, like thrusters or wheels. 109 | 110 | For using keypress functions, make sure that you have 2 keyframes, one for pressing a certain button, and another to release it later. You 111 | also can't press and release a key in 1 frame. Keypress function uses following keynames, and they must be separated with spaces if you 112 | want to activate multiple: 113 | 114 | 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , a , b , c , d , e , f , g , h , i , j , k , l , m , n , o , p , q , r , s , t , u , v , w , x , y , z , 115 | Numpad_0 , Numpad_1 , Numpad_2 , Numpad_3 , Numpad_4 , Numpad_5 , Numpad_6 , Numpad_7 , Numpad_8 , Numpad_9 , Numpad_/ , Numpad_* , Numpad_- , 116 | Numpad_+ , Numpad_Enter , Numpad_. , [ , ] , SEMICOLON , ' , ` , , , . , / , \ , - , = , ENTER , SPACE , BACKSPACE , TAB , CAPSLOCK , NUMLOCK , 117 | ESCAPE , SCROLLLOCK , INS , DEL , HOME , END , PGUP , PGDN , PAUSE , SHIFT , RSHIFT , ALT , RALT , CTRL , RCTRL , LWIN , RWIN , APP , UPARROW , 118 | LEFTARROW , DOWNARROW , RIGHTARROW , F1 , F2 , F3 , F4 , F5 , F6 , F7 , F8 , F9 , F10 , F11 , F12 , CAPSLOCKTOGGLE , NUMLOCKTOGGLE , SCROLLLOCKTOGGLE 119 | 120 | **NOTE:** Some console commands can't be used since they are [blocked](https://wiki.facepunch.com/gmod/Blocked_ConCommands) due to safety reasons. 121 | 122 | ## Animation playback 123 | 124 | To preview your current scene, you can bind `+smh_playback` command to a key and hold it down. This will play the animation from frame 0 to your frame count with the framerate specified in the `Framerate` input. Note that previewing your animation with this 125 | might not give an accurate display of the final animation. There may be slight lagging that will not be present when rendering the animation into images. 126 | 127 | ## Rendering 128 | 129 | You can render your animation by simply using a camera and cycling through all of the frames and take pictures. However, this gets very 130 | repetitive and takes time. SMH has the `smh_makejpeg` command, which automates this tedious task for you! Simply bind it to a key, 131 | set up your camera and stuff, and fire away. After the rendering is complete, the resulted images are found in your local steam screenshots. 132 | 133 | Alternatively, you can use `smh_makescreenshot`, which is the same as `smh_makejpeg`, but uses the `screenshot`command internally. 134 | This allows you to render TGA images instead of JPEG. 135 | 136 | You can optionally input a number in those commands to start render from a certain frame. 137 | 138 | ## Saving 139 | 140 | You might want to continue animating later, or save your finished scene, just in case you want to come back and change things. You can 141 | save your scene using the save menu found in the SMH menu. In the save menu, you give it a unique name, or overwrite one of the existing 142 | saves, and then hit the save button. And you scene is saved! 143 | 144 | ## Deleting saves 145 | 146 | You can also delete saves from the Save menu, or by navigating to `garrysmod/data/smh` and deleting files there. 147 | 148 | ## Loading 149 | 150 | When you want to load your saved frames, you will need to do this individually for all entities. When you have selected an entity with SMH, 151 | open the load menu. Select your previously saved scene. You will then see a list of saved entities, identified by their model name or the name 152 | they were given in the Properties menu. Select the right entity and then hit load, and the entity should now have all the frames that were saved. 153 | 154 | ## Spawning 155 | 156 | In load menu, after you have selected a save, you can press Spawn button which will open the Spawn menu in SMH's main menu. From there you can select 157 | a saved entity from the right column, and it will spawn a preview ghost in its position it was recorded on first frame. Clicking spawn button there will 158 | spawn that entity, and apply saved keyframes to it. You also can offset entity's position to your viewpoint, while using position of some other saved 159 | entity from the left column as a reference point, if they have recorded keyframes with `Physical Bones` or `Rotation and Position` modifiers, 160 | and adjust its rotation and position and spawn it somewhere else. 161 | 162 | ## Backing up saves 163 | 164 | All saves are located in `garrysmod/data/smh`, so you can back up your saved scenes from there, or move scenes to somewhere 165 | else if they take up space in the save list. 166 | 167 | ## Quicksave 168 | 169 | It's always good practice to frequently save your scene, as there are many unexpected things that can happen. Garry's mod might crash, 170 | SMH might error out, your computer may crash or you have to lock down your house in case of a zombie apocalypse. Saving your scene 171 | manually might not be the most ideal thing to do. That's why there's the `smh_quicksave` command. Bind a key into this command and 172 | your scene will be saved with the name `quicksave_[your nickname]` when you press it. 173 | 174 | ## Settings menu 175 | 176 | There are several options you can change in the settings menu: 177 | `Freeze all` will keep all physical bones of a ragdoll frozen when positioning to a frame, even if they were not frozen when the frame was recorded. 178 | `Don't animate phys bones` will disable the animation of physical bones entirely. This can be used for puppeteering while playing a facial animation, 179 | for example. This might not be usable in stop motion, so you'd have to use recording software, like OBS Studio. 180 | `Disable Tweening` will disable SMH's automatic tweening between keyframes, which can be useful for blocking animation. 181 | `Smooth Playback` will try to run playback smoother, although it may be more performance heavy. 182 | `Enable world keyframes` will allow world keyframes to execute their commands and keypresses. 183 | 184 | ## Ghosts 185 | 186 | Ghosts are static objects that represent previous and next frames of an entity. They are useful for determining where you want to place your prop or 187 | ragdoll before recording a frame. You can enable ghosts from the settings menu. `Ghost previous frame` will display a ghost for the previous 188 | frame. `Ghost next frame` will display a ghost for the next frame. `Ghost all entities` will display ghosts for all entities that have any frames, 189 | and not just the selected entity. `Ghost transparency` can be used to change the visibility of the ghosts, 0 being invisible and 1 being fully visible. 190 | 191 | ## Onion skinning 192 | 193 | Onion skinning will display ghost-like objects representing all frames of an entity. This might be useful to visualize the animation flow of your prop or ragdoll. 194 | To use onion skinning, bind a key to command `smh_onionskin`. You can then toggle onion skinning on and off. The option `Ghost all entities` 195 | in the options menu applies to onion skinning as well, so enabling that will let you see all frames of all entities at the same time. 196 | 197 | ## Physics recorder 198 | 199 | Physics recorder menu can be accessed through settings menu, which would allow you to add the selected entity for the physics recorder, set up the settings for the 200 | physics recording and toggle the physics recorder. 201 | 202 | **NOTE:** As long as physics recorder is working, you will not be able to select any entity. 203 | 204 | ## That's all! 205 | 206 | You can report any bugs on the workshop page. Or you can also give any other feedback, it helps too! 207 | -------------------------------------------------------------------------------- /addon.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignore": [], 3 | "tags": [ 4 | "movie" 5 | ], 6 | "title": "Stop Motion Helper", 7 | "type": "tool" 8 | } -------------------------------------------------------------------------------- /docs/DESIGN.md: -------------------------------------------------------------------------------- 1 | # Technical design of Stop Motion Helper 2 | 3 | SMH code should follow these basic principles: 4 | 5 | - Code should be as client-authoritive as possible, all logic that can sensibly be done in client, should be 6 | - Communication between modules and client-server communication should happen through the Controller 7 | - SMH should not be dependent on any other addons, and support for other addons should be completely conditional 8 | 9 | ## Architecture overview 10 | 11 | ![Architecture](smh_architecture.png "Basic architecture") 12 | 13 | The central piece of SMH code architecture is the Controller. It is responsible for communication between modules and client/server. 14 | It also stores the state and settings, so that it is accessible by all modules. 15 | 16 | For example, say we issue the console command `smh_next`: 17 | - Concommand module invokes controller to move to next frame 18 | - Controller calculates what the next frame is (is it current+1 or back to first frame?) 19 | - Controller sends "set frame" command to server 20 | - Server controller receives command and invokes keyframe manager to configure entities 21 | - Server sends an ack message back to client 22 | - Client controller invokes an event corresponding to the ack 23 | - UI listens to the event and moves the playhead to the correct position 24 | 25 | ## Module details 26 | 27 | ### Server 28 | 29 | - **Controller** - Communicate with clients, receive requests from clients 30 | - **KeyframeManager** - Create, update and delete keyframes for players 31 | - **PlaybackManager** - Position entities for keyframe playback 32 | - **KeyframeData** - Data container for keyframes 33 | 34 | ### Client 35 | 36 | - **Controller** - Communicate with server, send commands to server 37 | - **Highlighter** - Render currently selected entity halo 38 | - **Renderer** - Screenshot rendering 39 | - **ConCommands** - Register and handle console commands 40 | - **UI** - Derma UI setup and logic 41 | - **Settings** - Register and handle settings (console variables) 42 | - **KeyframeData** - Data container for keyframes 43 | - **State** - Data container for client state (frame position, playback settings) 44 | 45 | ## Keyframe data 46 | 47 | On serverside, a keyframe consists of 48 | - Keyframe ID, a shared unique identifier of the keyframe 49 | - Frame, aka position of the keyframe 50 | - Easing data 51 | - Modifier data, aka entity state data for the keyframe 52 | 53 | Whenever updated keyframe data is sent to client, Modifier data is omitted. The client has to separately ask for modifier data to use for saving or ghost rendering. 54 | 55 | KeyframeData stores keyframes in the following format: 56 | 57 | ```lua 58 | { 59 | [player] = { 60 | Keyframes = { 61 | [ID] = [keyframe] 62 | }, 63 | Entities = { 64 | [Entity] = { 65 | [keyframe] 66 | } 67 | } 68 | } 69 | } 70 | ``` 71 | 72 | Keyframes can be used to lookup keyframe data with it's ID, and Entities can be used to lookup keyframe data for a specific entity. Both reference the same data (the same Lua table). 73 | -------------------------------------------------------------------------------- /docs/smh_architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Winded/StopMotionHelper/ffa24afd3cf24163e76b2299ea9a2b740d05594a/docs/smh_architecture.png -------------------------------------------------------------------------------- /lua/autorun/client/derma_utils.lua: -------------------------------------------------------------------------------- 1 | 2 | local PANEL = FindMetaTable("Panel") 3 | 4 | function PANEL:SetRelativePos(otherPanel, x, y) 5 | local posX, posY = otherPanel:GetPos() 6 | self:SetPos(posX + x, posY + y) 7 | end 8 | 9 | -- For DListView 10 | function PANEL:UpdateLines(lines, isfolder) 11 | 12 | local set = {} 13 | local existing = {} 14 | 15 | for k, line in pairs(lines) do -- turn lines stuff into a "sorting" table 16 | if isfolder then 17 | line = "\\" .. line 18 | end 19 | set[line] = true 20 | end 21 | 22 | for k, line in pairs(self:GetLines()) do -- first we remove lines that are missing from the sorting table 23 | if not set[line:GetValue(1)] and isfolder == line.IsFolder then 24 | local _, selected = self:GetSelectedLine() 25 | if selected == line then self:ClearSelection() end -- clear selection if the removed line was selected 26 | self:RemoveLine(line:GetID()) 27 | continue 28 | end 29 | existing[line:GetValue(1)] = true 30 | end 31 | 32 | for line, _ in pairs(set) do 33 | if existing[line] then continue end 34 | 35 | local line = self:AddLine(line) 36 | if isfolder then 37 | line.IsFolder = true 38 | end 39 | end 40 | self:SortByColumn(1) 41 | end 42 | 43 | -- For number wangs 44 | function PANEL:GetNumberStep() 45 | return self.Step or 1 46 | end 47 | function PANEL:SetNumberStep(step) 48 | self.Step = step 49 | self.Up.DoClick = function() self:SetValue(self:GetValue() + self.Step) end 50 | self.Down.DoClick = function() self:SetValue(self:GetValue() - self.Step) end 51 | end 52 | -------------------------------------------------------------------------------- /lua/autorun/smh.lua: -------------------------------------------------------------------------------- 1 | --- 2 | -- SMH Entry point. 3 | --- 4 | 5 | if SERVER then 6 | AddCSLuaFile("smh.lua") 7 | include("smh/server.lua") 8 | else 9 | include("smh/client.lua") 10 | end 11 | -------------------------------------------------------------------------------- /lua/autorun/translations.lua: -------------------------------------------------------------------------------- 1 | 2 | -- Bone translation functions, so we can change their functionality here in case the original ones fuck up even more 3 | 4 | function GetPhysBoneParent(entity, bone) 5 | local b = PhysBoneToBone(entity, bone); 6 | local i = 1; 7 | while true do 8 | b = entity:GetBoneParent(b); 9 | local parent = BoneToPhysBone(entity, b); 10 | if parent >= 0 and parent ~= bone then 11 | return parent; 12 | end 13 | i = i + 1; 14 | if i > 128 then --We've gone through all possible bones, so we get out. 15 | break; 16 | end 17 | end 18 | return -1; 19 | end 20 | 21 | function PhysBoneToBone(ent, bone) 22 | return ent:TranslatePhysBoneToBone(bone); 23 | end 24 | 25 | function BoneToPhysBone(ent, bone) 26 | for i = 0, ent:GetPhysicsObjectCount() - 1 do 27 | local b = ent:TranslatePhysBoneToBone(i); 28 | if bone == b then 29 | return i; 30 | end 31 | end 32 | return -1; 33 | end 34 | -------------------------------------------------------------------------------- /lua/smh/client.lua: -------------------------------------------------------------------------------- 1 | include("shared.lua") 2 | 3 | include("client/state.lua") 4 | 5 | include("client/derma/frame_panel.lua") 6 | include("client/derma/frame_pointer.lua") 7 | include("client/derma/load.lua") 8 | include("client/derma/physrecord.lua") 9 | include("client/derma/properties.lua") 10 | include("client/derma/save.lua") 11 | include("client/derma/settings.lua") 12 | include("client/derma/smh_menu.lua") 13 | include("client/derma/spawn.lua") 14 | include("client/derma/world_clicker.lua") 15 | 16 | include("client/concommands.lua") 17 | include("client/controller.lua") 18 | include("client/highlighter.lua") 19 | include("client/physrecord.lua") 20 | include("client/renderer.lua") 21 | include("client/settings.lua") 22 | include("client/ui.lua") 23 | -------------------------------------------------------------------------------- /lua/smh/client/concommands.lua: -------------------------------------------------------------------------------- 1 | local smh_startatone = CreateClientConVar("smh_startatone", 0, true, false, "Controls whether the timeline starts at 0 or 1.", 0, 1) 2 | local smh_render_cmd = CreateClientConVar("smh_render_cmd", "poster 1", true, false, "For smh_render, this string will be ran in the console for each frame.") 3 | CreateClientConVar("smh_currentpreset", "default", true, false) 4 | 5 | concommand.Add("+smh_menu", function() 6 | SMH.Controller.OpenMenu() 7 | end) 8 | 9 | concommand.Add("-smh_menu", function() 10 | SMH.Controller.CloseMenu() 11 | end) 12 | 13 | concommand.Add("smh_record", function() 14 | SMH.Controller.Record() 15 | end) 16 | 17 | concommand.Add("smh_delete", function() 18 | local frame = SMH.State.Frame 19 | local ids = SMH.UI.GetKeyframesOnFrame(frame) 20 | if not ids then return end 21 | 22 | SMH.Controller.DeleteKeyframe(ids) 23 | end) 24 | 25 | concommand.Add("smh_next", function() 26 | local pos = SMH.State.Frame + 1 27 | if pos >= SMH.State.PlaybackLength then 28 | pos = 0 29 | end 30 | SMH.Controller.SetFrame(pos) 31 | end) 32 | 33 | concommand.Add("smh_previous", function() 34 | local pos = SMH.State.Frame - 1 35 | if pos < 0 then 36 | pos = SMH.State.PlaybackLength - 1 37 | end 38 | SMH.Controller.SetFrame(pos) 39 | end) 40 | 41 | concommand.Add("+smh_playback", function() 42 | SMH.Controller.StartPlayback() 43 | end) 44 | 45 | concommand.Add("-smh_playback", function() 46 | SMH.Controller.StopPlayback() 47 | end) 48 | 49 | concommand.Add("smh_quicksave", function() 50 | SMH.Controller.QuickSave() 51 | end) 52 | 53 | local function StartRender(startFrame, renderCmd) 54 | if startFrame then 55 | startFrame = startFrame - smh_startatone:GetInt() -- Implicit string->number. Normalizes startFrame to 0-indexed if smh_startatone is set. 56 | if startFrame < 0 then startFrame = 0 end 57 | else 58 | startFrame = 0 59 | end 60 | 61 | if startFrame < SMH.State.PlaybackLength then 62 | SMH.Controller.ToggleRendering(renderCmd, startFrame) 63 | else 64 | print("Specified starting frame is outside of the current Frame Count!") 65 | end 66 | end 67 | 68 | concommand.Add("smh_makejpeg", function(pl, cmd, args) 69 | StartRender(args[1], "jpeg") 70 | end) 71 | 72 | concommand.Add("smh_makescreenshot", function(pl, cmd, args) 73 | StartRender(args[1], "screenshot") 74 | end) 75 | 76 | concommand.Add("smh_render", function(pl, cmd, args) 77 | StartRender(args[1], smh_render_cmd:GetString()) 78 | end) -------------------------------------------------------------------------------- /lua/smh/client/derma/frame_panel.lua: -------------------------------------------------------------------------------- 1 | local PANEL = {} 2 | 3 | function PANEL:Init() 4 | 5 | self:SetBackgroundColor(Color(64, 64, 64, 64)) 6 | 7 | self.ScrollBar = vgui.Create("DPanel", self) 8 | self.ScrollBar.Paint = function(self, w, h) derma.SkinHook("Paint", "ScrollBarGrip", self, w, h) end 9 | self.ScrollBar.OnMousePressed = function(_, mousecode) self:OnScrollBarPressed(mousecode) end 10 | self.ScrollBar.OnMouseReleased = function(_, mousecode) self:OnScrollBarReleased(mousecode) end 11 | self.ScrollBar.OnCursorMoved = function(_, x, y) self:OnScrollBarCursorMoved(x, y) end 12 | 13 | self.ScrollButtonLeft = vgui.Create("DButton", self) 14 | self.ScrollButtonLeft:SetText("") 15 | self.ScrollButtonLeft.Paint = function(self, w, h) derma.SkinHook("Paint", "ButtonLeft", self, w, h) end 16 | self.ScrollButtonLeft.DoClick = function() self:SetScrollOffset(self.ScrollOffset - 1) end 17 | 18 | self.ScrollButtonRight = vgui.Create("DButton", self) 19 | self.ScrollButtonRight:SetText("") 20 | self.ScrollButtonRight.Paint = function(self, w, h) derma.SkinHook("Paint", "ButtonRight", self, w, h) end 21 | self.ScrollButtonRight.DoClick = function() self:SetScrollOffset(self.ScrollOffset + 1) end 22 | 23 | self.Zoom = 100 24 | self.TotalFrames = 100 25 | self.ScrollOffset = 0 26 | self.FrameArea = {0, 1} 27 | self._draggingScrollBar = false 28 | self._scrollCursorOffset = 0 29 | 30 | self.FramePointers = {} 31 | 32 | end 33 | 34 | function PANEL:PerformLayout(width, height) 35 | 36 | local frameAreaPadding = 10 37 | local scrollPadding = 18 38 | local scrollHeight = 12 39 | local scrollPosY = self:GetTall() - scrollHeight 40 | 41 | self.ScrollBarRect = { 42 | X = scrollPadding, 43 | Y = scrollPosY, 44 | Width = self:GetWide() - scrollPadding * 2, 45 | Height = scrollHeight, 46 | } 47 | 48 | self.ScrollButtonLeft:SetPos(scrollPadding - 12, scrollPosY) 49 | self.ScrollButtonLeft:SetSize(12, scrollHeight) 50 | 51 | self.ScrollButtonRight:SetPos(scrollPadding + self.ScrollBarRect.Width, scrollPosY) 52 | self.ScrollButtonRight:SetSize(12, scrollHeight) 53 | 54 | local startPoint = frameAreaPadding 55 | local endPoint = self:GetWide() - frameAreaPadding 56 | self.FrameArea = {startPoint, endPoint} 57 | 58 | self:RefreshScrollBar() 59 | self:RefreshFrames() 60 | 61 | end 62 | 63 | function PANEL:Paint(width, height) 64 | 65 | local startX, endX = unpack(self.FrameArea) 66 | local frameWidth = (endX - startX) / (self.Zoom - 1) 67 | 68 | surface.SetDrawColor(255, 255, 255, 255) 69 | for i = 0, self.Zoom - 1 do 70 | if self.ScrollOffset + i < self.TotalFrames then 71 | local x = startX + frameWidth * i 72 | surface.DrawLine(x, 6, x, height - 6) 73 | end 74 | end 75 | 76 | end 77 | 78 | function PANEL:UpdateFrameCount(totalframes) 79 | self.TotalFrames = totalframes 80 | 81 | if not self.ScrollBarRect then return end --check if we actually initialized the panel 82 | self:RefreshScrollBar() 83 | end 84 | 85 | function PANEL:RefreshScrollBar() 86 | if self.TotalFrames == self.Zoom then 87 | self.ScrollBar:SetPos(self.ScrollBarRect.X, self.ScrollBarRect.Y) 88 | self.ScrollBar:SetSize(self.ScrollBarRect.Width, self.ScrollBarRect.Height) 89 | return 90 | end 91 | 92 | local barWidthPerc = self.Zoom / self.TotalFrames 93 | barWidthPerc = barWidthPerc > 1 and 1 or barWidthPerc 94 | 95 | local barXPerc = self.ScrollOffset / (self.TotalFrames - self.Zoom) 96 | barXPerc = barXPerc < 0 and 0 or (barXPerc > 1 and 1 or barXPerc) 97 | 98 | local width = self.ScrollBarRect.Width * barWidthPerc 99 | local height = self.ScrollBarRect.Height 100 | local x = self.ScrollBarRect.X + (self.ScrollBarRect.Width - width) * barXPerc 101 | local y = self.ScrollBarRect.Y 102 | 103 | self.ScrollBar:SetPos(x, y) 104 | self.ScrollBar:SetSize(width, height) 105 | end 106 | 107 | function PANEL:RefreshFrames() 108 | for _, pointer in pairs(self.FramePointers) do 109 | pointer:RefreshFrame() 110 | end 111 | end 112 | 113 | function PANEL:SetScrollOffset(offset) 114 | if offset < 0 then 115 | offset = 0 116 | elseif offset >= self.TotalFrames then 117 | offset = self.TotalFrames - 1 118 | end 119 | 120 | self.ScrollOffset = offset 121 | self:RefreshScrollBar() 122 | self:RefreshFrames() 123 | end 124 | 125 | function PANEL:CreateFramePointer(color, verticalPosition, pointyBottom) 126 | local pointer = vgui.Create("SMHFramePointer", self) 127 | pointer.Color = color 128 | pointer.VerticalPosition = verticalPosition 129 | pointer.PointyBottom = pointyBottom 130 | table.insert(self.FramePointers, pointer) 131 | 132 | return pointer 133 | end 134 | 135 | function PANEL:DeleteFramePointer(pointer) 136 | table.RemoveByValue(self.FramePointers, pointer) 137 | pointer:Remove() 138 | end 139 | 140 | function PANEL:OnMousePressed(mousecode) 141 | if mousecode ~= MOUSE_LEFT then 142 | return 143 | end 144 | 145 | local startX, endX = unpack(self.FrameArea) 146 | local posX, posY = self:CursorPos() 147 | 148 | local targetX = posX - startX 149 | local width = endX - startX 150 | local framePosition = math.Round(self.ScrollOffset + (targetX / width) * (self.Zoom - 1)) 151 | framePosition = framePosition < 0 and 0 or (framePosition >= self.TotalFrames and self.TotalFrames - 1 or framePosition) 152 | 153 | self:OnFramePressed(framePosition) 154 | end 155 | 156 | function PANEL:OnMouseWheeled(scrollDelta) 157 | scrollDelta = -scrollDelta 158 | local newZoom = self.Zoom + scrollDelta 159 | if newZoom > 500 then 160 | newZoom = 500 161 | elseif newZoom < 30 then 162 | newZoom = 30 163 | end 164 | 165 | self.Zoom = newZoom 166 | self:RefreshFrames() 167 | self:RefreshScrollBar() 168 | end 169 | 170 | function PANEL:OnScrollBarPressed(mousecode) 171 | if mousecode ~= MOUSE_LEFT then 172 | return 173 | end 174 | 175 | self.ScrollBar:MouseCapture(true) 176 | self._draggingScrollBar = true 177 | 178 | local cursorXOffset, _ = self.ScrollBar:CursorPos() 179 | self._scrollCursorOffset = cursorXOffset 180 | end 181 | 182 | function PANEL:OnScrollBarReleased(mousecode) 183 | if mousecode ~= MOUSE_LEFT then 184 | return 185 | end 186 | 187 | self.ScrollBar:MouseCapture(false) 188 | self._draggingScrollBar = false 189 | end 190 | 191 | function PANEL:OnScrollBarCursorMoved(x, y) 192 | if not self._draggingScrollBar then 193 | return 194 | end 195 | 196 | local cursorX, _ = self:CursorPos() 197 | local movePos = cursorX - self._scrollCursorOffset - self.ScrollBarRect.X 198 | 199 | local movableWidth = self.ScrollBarRect.Width - self.ScrollBar:GetWide() 200 | if movableWidth ~= 0 then 201 | local numSteps = self.TotalFrames - self.Zoom 202 | local targetScrollOffset = math.Round((movePos / movableWidth) * numSteps) 203 | 204 | if targetScrollOffset >= 0 and targetScrollOffset <= numSteps and targetScrollOffset ~= self.ScrollOffset then 205 | self:SetScrollOffset(targetScrollOffset) 206 | end 207 | elseif self.ScrollOffset ~= 0 then 208 | self:SetScrollOffset(0) 209 | end 210 | end 211 | 212 | function PANEL:OnFramePressed(frame) end 213 | 214 | vgui.Register("SMHFramePanel", PANEL, "DPanel") 215 | -------------------------------------------------------------------------------- /lua/smh/client/derma/frame_pointer.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | TODO move control key logic elsewhere 3 | 4 | local leftMousePressStream = mousePressStream:filter(function(mousecode) return mousecode == MOUSE_LEFT end); 5 | local leftMouseReleaseStream = mouseReleaseStream:filter(function(mousecode) return mousecode == MOUSE_LEFT end); 6 | local rightMousePressStream = mousePressStream 7 | :filter(function(mousecode) return mousecode == MOUSE_RIGHT and not input.IsKeyDown(KEY_LCONTROL) end); 8 | local middleMousePressStream = mousePressStream 9 | :filter(function(mousecode) return mousecode == MOUSE_MIDDLE or (mousecode == MOUSE_RIGHT and input.IsKeyDown(KEY_LCONTROL)) end); 10 | local middleMouseReleaseStream = mouseReleaseStream 11 | :filter(function(mousecode) return mousecode == MOUSE_MIDDLE or (mousecode == MOUSE_RIGHT and input.IsKeyDown(KEY_LCONTROL)) end); 12 | ]] 13 | 14 | local PANEL = {} 15 | 16 | function PANEL:Init() 17 | 18 | self:SetSize(8, 15) 19 | self.Color = Color(0, 200, 0) 20 | self.OutlineColor = Color(0, 0, 0) 21 | self.OutlineColorDragged = Color(255, 255, 255) 22 | self.VerticalPosition = 0 23 | self.PointyBottom = false 24 | 25 | self._frame = 0 26 | self._dragging = false 27 | self._ids = {} 28 | self._ent = {} 29 | self._selected = false 30 | self._maxoffset = 0 31 | self._minoffset = 0 32 | 33 | end 34 | 35 | function PANEL:Paint(width, height) 36 | local parent = self:GetParent() 37 | if self._frame < parent.ScrollOffset or self._frame > (parent.ScrollOffset + parent.Zoom - 1) then 38 | return 39 | end 40 | 41 | local outlineColor = ((self._selected or self._dragging) and self.OutlineColorDragged) or self.OutlineColor 42 | 43 | if self.PointyBottom then 44 | 45 | surface.SetDrawColor(self.Color:Unpack()) 46 | draw.NoTexture() 47 | surface.DrawRect(1, 1, width - 1, height - (height * 0.25)) 48 | surface.DrawPoly({ 49 | { x = 1, y = height - (height * 0.25) }, 50 | { x = width - 1, y = height - (height * 0.25) }, 51 | { x = width / 2, y = height - 1 }, 52 | }) 53 | 54 | surface.SetDrawColor(outlineColor:Unpack()) 55 | surface.DrawLine(0, 0, width, 0) 56 | surface.DrawLine(width, 0, width, height - (height * 0.25)) 57 | surface.DrawLine(width, height - (height * 0.25), width / 2, height) 58 | surface.DrawLine(width / 2, height, 0, height - (height * 0.25)) 59 | surface.DrawLine(0, height - (height * 0.25), 0, 0) 60 | 61 | else 62 | 63 | surface.SetDrawColor(self.Color:Unpack()) 64 | surface.DrawRect(1, 1, width - 1, height - 1) 65 | 66 | surface.SetDrawColor(outlineColor:Unpack()) 67 | surface.DrawLine(0, 0, width, 0) 68 | surface.DrawLine(width, 0, width, height) 69 | surface.DrawLine(width, height, 0, height) 70 | surface.DrawLine(0, height, 0, 0) 71 | 72 | end 73 | end 74 | 75 | function PANEL:GetFrame() 76 | return self._frame 77 | end 78 | 79 | function PANEL:SetFrame(frame) 80 | local parent = self:GetParent() 81 | 82 | local startX, endX = unpack(parent.FrameArea) 83 | local height = self.VerticalPosition 84 | 85 | local frameAreaWidth = endX - startX 86 | local offsetFrame = frame - parent.ScrollOffset 87 | local x = startX + (offsetFrame / (parent.Zoom - 1)) * frameAreaWidth 88 | 89 | self:SetPos(x - self:GetWide() / 2, height - self:GetTall() / 2) 90 | self._frame = frame 91 | end 92 | 93 | function PANEL:RefreshFrame() 94 | self:SetFrame(self._frame) 95 | end 96 | 97 | function PANEL:IsDragging() 98 | return self._dragging 99 | end 100 | 101 | function PANEL:SetSelected(selected) 102 | self._selected = selected 103 | end 104 | 105 | function PANEL:GetSelected() 106 | return self._selected 107 | end 108 | 109 | function PANEL:GetIDs() 110 | return self._ids 111 | end 112 | 113 | function PANEL:GetEnts() 114 | return self._ent 115 | end 116 | 117 | function PANEL:RemoveID(id) 118 | self._ent[self._ids[id]] = nil 119 | self._ids[id] = nil 120 | end 121 | 122 | function PANEL:AddID(id, mod) 123 | self._ids[id] = mod 124 | self._ent[mod] = id 125 | end 126 | 127 | function PANEL:OnMousePressed(mousecode) 128 | if mousecode ~= MOUSE_LEFT then 129 | self:MouseCapture(false) 130 | self._dragging = false 131 | self:OnCustomMousePressed(mousecode) 132 | return 133 | end 134 | 135 | self:MouseCapture(true) 136 | self._dragging = true 137 | 138 | SMH.UI.SetOffsets(self) 139 | end 140 | 141 | function PANEL:SetParentPointer(ppointer) 142 | self._parent = ppointer 143 | end 144 | 145 | function PANEL:ClearParentPointer() 146 | self._parent = nil 147 | end 148 | 149 | function PANEL:GetParentKeyframe() 150 | return self._parent 151 | end 152 | 153 | function PANEL:SetOffsets(minimum, maximum) 154 | self._minoffset = minimum 155 | self._maxoffset = maximum 156 | end 157 | 158 | function PANEL:OnMouseReleased(mousecode) 159 | if not self._dragging then 160 | return 161 | end 162 | 163 | self:SetOffsets(0, 0) 164 | 165 | self:MouseCapture(false) 166 | self._dragging = false 167 | --SMH.UI.ClearFrames(self) 168 | self:OnPointerReleased(self._frame) 169 | 170 | if mousecode == MOUSE_LEFT and not self.PointyBottom then 171 | if input.IsKeyDown(KEY_LSHIFT) then 172 | SMH.UI.ShiftSelect(self) 173 | elseif input.IsKeyDown(KEY_LCONTROL) then 174 | SMH.UI.ToggleSelect(self) 175 | else 176 | SMH.UI.ClearAllSelected() 177 | end 178 | end 179 | end 180 | 181 | function PANEL:OnCursorMoved() 182 | if not self._dragging then 183 | return 184 | end 185 | 186 | local parent = self:GetParent() 187 | 188 | local cursorX, cursorY = parent:CursorPos() 189 | local startX, endX = unpack(parent.FrameArea) 190 | 191 | local targetX = cursorX - startX 192 | local width = endX - startX 193 | 194 | local targetPos = math.Round(parent.ScrollOffset + (targetX / width) * (parent.Zoom - 1)) 195 | targetPos = targetPos < 0 - self._minoffset and 0 - self._minoffset or (targetPos >= parent.TotalFrames - self._maxoffset and parent.TotalFrames - 1 - self._maxoffset or targetPos) 196 | 197 | if targetPos ~= self._frame then 198 | SMH.UI.MoveChildren(self, targetPos) 199 | self:SetFrame(targetPos) 200 | self:OnFrameChanged(targetPos) 201 | SMH.UI.MoveChildren(self, targetPos) 202 | end 203 | end 204 | 205 | function PANEL:OnFrameChanged(newFrame) end 206 | function PANEL:OnPointerReleased(frame) end 207 | function PANEL:OnCustomMousePressed(mousecode) end 208 | 209 | vgui.Register("SMHFramePointer", PANEL, "DPanel") 210 | -------------------------------------------------------------------------------- /lua/smh/client/derma/load.lua: -------------------------------------------------------------------------------- 1 | local PANEL = {} 2 | 3 | function PANEL:Init() 4 | 5 | self:SetTitle("Load") 6 | self:SetDeleteOnClose(false) 7 | self:SetSizable(true) 8 | 9 | self:SetSize(250, 250) 10 | self:SetMinWidth(250) 11 | self:SetMinHeight(250) 12 | self:SetPos(ScrW() / 2 - self:GetWide() / 2, ScrH() / 2 - self:GetTall() / 2) 13 | 14 | self.PathLabel = vgui.Create("DLabel", self) 15 | self.PathLabel:SetMouseInputEnabled(true) 16 | self.PathLabel:SetText("smh/") 17 | self.PathLabel:SetTooltip("smh/") 18 | 19 | self.FileList = vgui.Create("DListView", self) 20 | self.FileList:AddColumn("Saved scenes") 21 | self.FileList:SetMultiSelect(false) 22 | self.FileList.OnRowSelected = function(_, rowIndex, row) 23 | if row.IsFolder or row:GetValue(1) == ".." then 24 | return 25 | end 26 | self:OnModelListRequested(row:GetValue(1), false) 27 | end 28 | self.FileList.DoDoubleClick = function(_, rowIndex, row) 29 | local path = row:GetValue(1) 30 | if not (row.IsFolder or path == "..") then return end 31 | if row.IsFolder then path = string.sub(path, 2) end 32 | 33 | self:DoFolderPath(path) 34 | end 35 | 36 | self.EntityList = vgui.Create("DListView", self) 37 | self.EntityList:AddColumn("Entities") 38 | self.EntityList:SetMultiSelect(false) 39 | self.EntityList.OnRowSelected = function(_, rowIndex, row) 40 | local _, selectedSave = self.FileList:GetSelectedLine() 41 | if not IsValid(selectedSave) then return end 42 | self:OnModelInfoRequested(selectedSave:GetValue(1),row:GetValue(1), false) 43 | end 44 | 45 | self.Load = vgui.Create("DButton", self) 46 | self.Load:SetText("Load") 47 | self.Load.DoClick = function() 48 | self:LoadSelected() 49 | end 50 | 51 | self.Spawn = vgui.Create("DButton", self) 52 | self.Spawn:SetText("Spawn") 53 | self.Spawn.DoClick = function() 54 | self:OpenSpawnMenu() 55 | end 56 | 57 | self.SaveEntity = vgui.Create("DLabel", self) 58 | self.SaveEntity:SetText("Save's model: " .. "nil") 59 | 60 | self.SaveClass = vgui.Create("DLabel", self) 61 | self.SaveClass:SetText("Save's class: " .. "nil") 62 | 63 | self.SaveMap = vgui.Create("DLabel", self) 64 | self.SaveMap:SetText("Save's map: " .. "nil") 65 | 66 | self.SelectedEnt = vgui.Create("DLabel", self) 67 | self.SelectedEnt:SetText("Selected model: " .. "nil") 68 | 69 | end 70 | 71 | function PANEL:PerformLayout(width, height) 72 | 73 | self.BaseClass.PerformLayout(self, width, height) 74 | 75 | self.PathLabel:SetPos(5, 30) 76 | self.PathLabel:SetSize(self:GetWide(), 15) 77 | 78 | self.FileList:SetPos(5, 45) 79 | self.FileList:SetSize(117 + ( self:GetWide()/2 - 125 ), 120 + ( self:GetTall()*0.9 - 225 )) 80 | 81 | self.EntityList:SetPos(127 + ( self:GetWide()/2 - 125 ), 45) 82 | self.EntityList:SetSize(117 + ( self:GetWide()/2 - 125 ), 120 + ( self:GetTall()*0.9 - 225 )) 83 | 84 | self.Load:SetPos(self:GetWide() - 65 - ( self:GetWide()*0.2 - 50 ), self:GetTall() - 58 - ( self:GetTall()*0.1 - 25 )) 85 | self.Load:SetSize(60 + ( self:GetWide()*0.2 - 50 ), 20 + ( self:GetTall()*0.1 - 25 )/2) 86 | 87 | self.Spawn:SetRelativePos(self.Load, 0, 30 + ( self:GetTall()*0.1 - 25 )/2) 88 | self.Spawn:SetSize(60 + ( self:GetWide()*0.2 - 50 ), 20 + ( self:GetTall()*0.1 - 25 )/2) 89 | 90 | local labelSize, labelY = self.Load:GetX() - 10, self:GetTall()*0.9 - 225 91 | 92 | self.SelectedEnt:SetPos(5, 230 + labelY) 93 | self.SelectedEnt:SetSize(labelSize, 15) 94 | 95 | self.SaveEntity:SetPos(5, 170 + labelY) 96 | self.SaveEntity:SetSize(labelSize, 15) 97 | 98 | self.SaveClass:SetPos(5, 190 + labelY) 99 | self.SaveClass:SetSize(labelSize, 15) 100 | 101 | self.SaveMap:SetPos(5, 210 + labelY) 102 | self.SaveMap:SetSize(labelSize, 15) 103 | 104 | end 105 | 106 | function PANEL:DoFolderPath(path) 107 | if not path or path == "" then 108 | return 109 | end 110 | 111 | self.EntityList:Clear() 112 | 113 | self:OnGoToFolderRequested(path) 114 | end 115 | 116 | function PANEL:UpdateSelectedEnt(ent) 117 | local SelectedName = ent == LocalPlayer() and "world" or IsValid(ent) and ent:GetModel() or "nil" 118 | self.SelectedEnt:SetText("Selected model: " .. SelectedName) 119 | end 120 | 121 | function PANEL:SetSaves(folders, saves, path) 122 | self.FileList:UpdateLines(folders, true) 123 | self.FileList:UpdateLines(saves) 124 | self.PathLabel:SetText(path) 125 | self.PathLabel:SetTooltip(path) 126 | 127 | local kablooey = string.Explode("/", path) 128 | if #kablooey > 2 then 129 | local line = self.FileList:AddLine("..") 130 | self.FileList:SortByColumn(1) 131 | end 132 | end 133 | 134 | function PANEL:SetEntities(entities, map) 135 | self.EntityList:UpdateLines(entities) 136 | self.SaveMap:SetText("Selected map: " .. map) 137 | end 138 | 139 | function PANEL:SetModelName(name, class) 140 | self.SaveEntity:SetText("Save's model: " .. name) 141 | self.SaveClass:SetText("Save's class: " .. class) 142 | end 143 | 144 | function PANEL:LoadSelected() 145 | local _, selectedSave = self.FileList:GetSelectedLine() 146 | local _, selectedEntity = self.EntityList:GetSelectedLine() 147 | 148 | -- TODO clientside support for loading and saving 149 | 150 | if not IsValid(selectedSave) or not IsValid(selectedEntity) then 151 | return 152 | end 153 | 154 | -- TODO clientside support for loading and saving 155 | self:OnLoadRequested(selectedSave:GetValue(1), selectedEntity:GetValue(1), false) 156 | end 157 | 158 | function PANEL:OpenSpawnMenu() end 159 | function PANEL:OnModelListRequested(path, loadFromClient) end 160 | function PANEL:OnGoToFolderRequested(path, toClient) end 161 | function PANEL:OnLoadRequested(path, modelName, loadFromClient) end 162 | function PANEL:OnModelInfoRequested(path, modelname, loadFromClient) end 163 | 164 | vgui.Register("SMHLoad", PANEL, "DFrame") 165 | -------------------------------------------------------------------------------- /lua/smh/client/derma/physrecord.lua: -------------------------------------------------------------------------------- 1 | local PANEL = {} 2 | 3 | function PANEL:Init() 4 | 5 | local function CreateSlider(label, min, max, default, func) 6 | local slider = vgui.Create("DNumSlider", self) 7 | 8 | -- overriding default functions as it used to clamp result between mix and max, and we kinda want to go over the max if need be 9 | slider.SetValue = function(self, val) 10 | 11 | if ( self:GetValue() == val ) then return end 12 | 13 | self.Scratch:SetValue( val ) 14 | 15 | self:ValueChanged( self:GetValue() ) 16 | 17 | end 18 | 19 | slider.ValueChanged = function(self, val) 20 | 21 | if ( self.TextArea != vgui.GetKeyboardFocus() ) then 22 | self.TextArea:SetValue( self.Scratch:GetTextValue() ) 23 | end 24 | 25 | self.Slider:SetSlideX( self.Scratch:GetFraction( val ) ) 26 | 27 | self:OnValueChanged( val ) 28 | 29 | end 30 | 31 | slider:SetMinMax(min, max) 32 | slider:SetDecimals(0) 33 | slider:SetDefaultValue(default) 34 | slider:SetValue(default) 35 | slider:SetText(label) 36 | slider.OnValueChanged = func 37 | slider:GetTextArea().OnValueChange = func 38 | return slider 39 | end 40 | 41 | self:SetTitle("SMH Physics Recorder") 42 | self:SetDeleteOnClose(false) 43 | 44 | self.FrameAmount = CreateSlider("Record Frame Count", 1, 200, 100, function(_, value) 45 | value = tonumber(value) 46 | if not value then return end 47 | 48 | if value < 3 then 49 | value = 3 50 | end 51 | SMH.PhysRecord.FrameCount = math.Round(value) 52 | end) 53 | 54 | self.Interval = CreateSlider("Record Interval", 0, 20, 0, function(_, value) 55 | value = tonumber(value) 56 | if not value then return end 57 | 58 | if value < 0 then 59 | value = 0 60 | end 61 | SMH.PhysRecord.RecordInterval = math.Round(value + 1) 62 | end) 63 | 64 | self.Delay = CreateSlider("Delay", 1, 10, 3, function(_, value) 65 | value = tonumber(value) 66 | if not value then return end 67 | 68 | if value < 0 then 69 | value = 0 70 | end 71 | SMH.PhysRecord.StartDelay = math.Round(value) 72 | end) 73 | 74 | self.RecordButton = vgui.Create("DButton", self) 75 | self.RecordButton:SetText("Toggle Record") 76 | self.RecordButton.DoClick = function() 77 | SMH.PhysRecord.RecordToggle() 78 | self.SelectEntity:SetText("Select Entity") 79 | 80 | if not next(SMH.State.Entity) then return end 81 | for entity, _ in pairs(SMH.State.Entity) do 82 | SMH.PhysRecord.SelectedEntities[entity] = SMH.State.Timeline 83 | end 84 | end 85 | 86 | self.SelectEntity = vgui.Create("DButton", self) 87 | self.SelectEntity:SetText("Select Entity") 88 | self.SelectEntity.DoClick = function() 89 | if not next(SMH.State.Entity) then return end 90 | local entity = next(SMH.State.Entity) 91 | 92 | if not SMH.PhysRecord.SelectedEntities[entity] then 93 | SMH.PhysRecord.SelectedEntities[entity] = SMH.State.Timeline 94 | self.SelectEntity:SetText("Unselect Entity") 95 | else 96 | SMH.PhysRecord.SelectedEntities[entity] = nil 97 | self.SelectEntity:SetText("Select Entity") 98 | end 99 | end 100 | 101 | self.RemoveAllSelected = vgui.Create("DButton", self) 102 | self.RemoveAllSelected:SetText("Clear all selected") 103 | self.RemoveAllSelected.DoClick = function() 104 | SMH.PhysRecord.SelectedEntities = {} 105 | self.SelectEntity:SetText("Select Entity") 106 | end 107 | 108 | self:SetSize(250, 170) 109 | 110 | end 111 | 112 | function PANEL:PerformLayout(width, height) 113 | 114 | self.BaseClass.PerformLayout(self, width, height) 115 | 116 | self.FrameAmount:SetPos(5, 25) 117 | self.FrameAmount:SetSize(self:GetWide() - 10, 25) 118 | 119 | self.Interval:SetPos(5, 55) 120 | self.Interval:SetSize(self:GetWide() - 10, 25) 121 | 122 | self.Delay:SetPos(5, 85) 123 | self.Delay:SetSize(self:GetWide() - 10, 25) 124 | 125 | self.SelectEntity:SetPos(5, 115) 126 | self.SelectEntity:SetSize(self:GetWide() / 2 - 15, 20) 127 | 128 | self.RemoveAllSelected:SetPos(10 + self:GetWide() / 2, 115) 129 | self.RemoveAllSelected:SetSize(self:GetWide() / 2 - 15, 20) 130 | 131 | self.RecordButton:SetPos(5, 140) 132 | self.RecordButton:SetSize(self:GetWide() - 10, 20) 133 | 134 | end 135 | 136 | function PANEL:UpdateSelectedEnt(entity) 137 | if not IsValid(entity) then 138 | self.SelectEntity:SetText("Select Entity") 139 | return 140 | end 141 | 142 | if not SMH.PhysRecord.SelectedEntities[entity] then 143 | self.SelectEntity:SetText("Select Entity") 144 | else 145 | self.SelectEntity:SetText("Unselect Entity") 146 | end 147 | end 148 | 149 | vgui.Register("SMHPhysRecord", PANEL, "DFrame") 150 | -------------------------------------------------------------------------------- /lua/smh/client/derma/properties.lua: -------------------------------------------------------------------------------- 1 | local PANEL = {} 2 | local EntsTable = {} 3 | local PropertyTable = {} 4 | local ModifierList = {} 5 | local Fallback = "none" 6 | local selectedEntity = nil 7 | local UsingWorld = false 8 | local IsSaving = false 9 | 10 | local function GetModelName(entity) 11 | local mdl = string.Split(entity:GetModel(), "/"); 12 | mdl = mdl[#mdl]; 13 | return mdl 14 | end 15 | 16 | local function FindEntityInfo(entity) 17 | if EntsTable then 18 | for kentity, value in pairs(EntsTable) do 19 | if kentity == entity then 20 | return value 21 | end 22 | end 23 | end 24 | 25 | return nil 26 | end 27 | 28 | local function FindEntity(name) 29 | if EntsTable then 30 | for kentity, value in pairs(EntsTable) do 31 | if value.Name == name then 32 | return kentity 33 | end 34 | end 35 | end 36 | 37 | return nil 38 | end 39 | 40 | local function UpdateName(name) 41 | if not IsValid(selectedEntity) then return end 42 | if EntsTable then 43 | EntsTable[selectedEntity].Name = name 44 | end 45 | end 46 | 47 | function PANEL:Init() 48 | 49 | self:SetTitle("Properties") 50 | self:SetDeleteOnClose(false) 51 | self:SetSizable(true) 52 | 53 | self:SetSize(704, 420) 54 | self:SetMinWidth(704) 55 | self:SetMinHeight(420) 56 | self:SetPos(ScrW() / 2 - self:GetWide() / 2, ScrH() / 2 - self:GetTall() / 2) 57 | 58 | self.EntitiesPanel = vgui.Create("DPanel", self) 59 | self.EntitiesPanel:SetBackgroundColor(Color(155, 155, 155, 255)) 60 | 61 | self.EntityNameEnter = vgui.Create("DTextEntry", self.EntitiesPanel) 62 | self.EntityNameEnter:SetSize(236, 20) 63 | self.EntityNameEnter:SetEditable(false) 64 | self.EntityNameEnter:SetText("none") 65 | self.EntityNameEnter.OnLoseFocus = function(sel) 66 | if sel:GetValue() == "" then 67 | sel:SetText(Fallback) 68 | end 69 | 70 | self:ApplyName(selectedEntity, sel:GetValue()) 71 | end 72 | self.EntityNameEnter.Label = vgui.Create("DLabel", self.EntitiesPanel) 73 | self.EntityNameEnter.Label:SetText("Selected entity's name:") 74 | self.EntityNameEnter.Label:SizeToContents() 75 | 76 | self.EntityList = vgui.Create("DListView", self.EntitiesPanel) 77 | self.EntityList:AddColumn("Recorded Entities") 78 | self.EntityList:SetMultiSelect(false) 79 | self.EntityList.OnRowSelected = function(_, rowIndex, row) 80 | local _, selectedName = self.EntityList:GetSelectedLine() 81 | if not IsValid(selectedName) then return end 82 | local selectedEntity = FindEntity(selectedName:GetValue(1)) 83 | if not IsValid(selectedEntity) then return end 84 | self:SelectEntity(selectedEntity) 85 | end 86 | 87 | self.TimelinesPanel = vgui.Create("DPanel", self) 88 | self.TimelinesPanel:SetBackgroundColor(Color(155, 155, 155, 255)) 89 | 90 | self.SettingPicker = vgui.Create("DComboBox", self.TimelinesPanel) 91 | self.SettingPicker.OnSelect = function(_, index, value) 92 | RunConsoleCommand("smh_currentpreset", value) 93 | local settings = SMH.Saves.GetPreferences(value) 94 | if not settings and not value == "default" then return end 95 | self:SetSettings(settings, value) 96 | end 97 | 98 | self.AddSettingPresetButton = vgui.Create("DButton", self.TimelinesPanel) 99 | self.AddSettingPresetButton:SetText("+") 100 | self.AddSettingPresetButton.DoClick = function() 101 | self:MakeSettingSavePanel() 102 | end 103 | 104 | self.SelectedEntityLabel = vgui.Create("DLabel", self.TimelinesPanel) 105 | self.SelectedEntityLabel:SetText("Selected model: " .. "none") 106 | 107 | self.AddTimeButton = vgui.Create("DButton", self.TimelinesPanel) 108 | self.AddTimeButton:SetText("Add Timeline") 109 | self.AddTimeButton.DoClick = function() 110 | if UsingWorld then return end 111 | self:ButtonTimeline(true) 112 | end 113 | 114 | self.RemoveTimeButton = vgui.Create("DButton", self.TimelinesPanel) 115 | self.RemoveTimeButton:SetText("Remove Timeline") 116 | self.RemoveTimeButton.DoClick = function() 117 | if UsingWorld then return end 118 | self:ButtonTimeline(false) 119 | end 120 | 121 | self.TimelinesCList = vgui.Create("DCategoryList", self.TimelinesPanel) 122 | 123 | self.ColorPanel = vgui.Create("DPanel", self) 124 | self.ColorPanel:SetBackgroundColor(Color(155, 155, 155, 255)) 125 | 126 | self.ColorLabel = vgui.Create("DLabel", self.ColorPanel) 127 | self.ColorLabel:SetText("Keyframe Color for timeline: " .. "none") 128 | 129 | self.ColorPicker = vgui.Create("DColorMixer", self.ColorPanel) 130 | self.ColorPicker:SetPalette(false) 131 | self.ColorPicker:SetAlphaBar(false) 132 | self.ColorPicker:SetWangs(true) 133 | self.ColorPicker.Timeline = -1 134 | self.ColorPicker.ValueChanged = function(_, col) 135 | col = Color(col.r, col.g, col.b) 136 | self.ColorPreview:SetBackgroundColor(col) 137 | if next(PropertyTable) == nil or self.ColorPicker.Timeline == -1 then return end 138 | self:OnUpdateKeyframeColorRequested(col, self.ColorPicker.Timeline) 139 | if self.ColorPicker.Timeline == SMH.State.Timeline then SMH.UI.PaintKeyframes(col) end 140 | end 141 | 142 | self.ColorPreview = vgui.Create("DPanel", self.ColorPanel) 143 | self.ColorPreview:SetBackgroundColor(self.ColorPicker:GetColor()) 144 | 145 | self.SelectWorldButton = vgui.Create("DButton", self.ColorPanel) 146 | self.SelectWorldButton:SetText("Select World") 147 | self.SelectWorldButton.DoClick = function() 148 | self:SelectWorld() 149 | end 150 | 151 | self.WorldParent = vgui.Create("Panel", self.ColorPanel) 152 | 153 | self.ConsoleEnter = vgui.Create("DTextEntry", self.WorldParent) 154 | self.ConsoleEnter.OnLoseFocus = function(sel) 155 | self:SetData(sel:GetValue(), "Console") 156 | end 157 | self.ConsoleEnter.Label = vgui.Create("DLabel", self.WorldParent) 158 | self.ConsoleEnter.Label:SetText("Console command:") 159 | self.ConsoleEnter.Label:SizeToContents() 160 | 161 | self.ButtonPressEnter = vgui.Create("DTextEntry", self.WorldParent) 162 | self.ButtonPressEnter.OnLoseFocus = function(sel) 163 | self:SetData(sel:GetValue(), "Push") 164 | end 165 | self.ButtonPressEnter.Label = vgui.Create("DLabel", self.WorldParent) 166 | self.ButtonPressEnter.Label:SetText("Keys to press:") 167 | self.ButtonPressEnter.Label:SizeToContents() 168 | 169 | self.ButtonReleaseEnter = vgui.Create("DTextEntry", self.WorldParent) 170 | self.ButtonReleaseEnter.OnLoseFocus = function(sel) 171 | self:SetData(sel:GetValue(), "Release") 172 | end 173 | self.ButtonReleaseEnter.Label = vgui.Create("DLabel", self.WorldParent) 174 | self.ButtonReleaseEnter.Label:SetText("Keys to release:") 175 | self.ButtonReleaseEnter.Label:SizeToContents() 176 | 177 | self.WorldParent:SetVisible(false) 178 | 179 | end 180 | 181 | function PANEL:PerformLayout(width, height) 182 | 183 | self.BaseClass.PerformLayout(self, width, height) 184 | 185 | self.EntitiesPanel:SetPos(4, 30) 186 | self.EntitiesPanel:SetSize(240, self:GetTall() - 4 - 30) 187 | 188 | self.EntityNameEnter:SetPos(2, 25) 189 | self.EntityNameEnter.Label:SetRelativePos(self.EntityNameEnter, 2, -5 - self.EntityNameEnter.Label:GetTall()) 190 | 191 | self.EntityList:SetPos(5, 60) 192 | self.EntityList:SetSize(230, self.EntitiesPanel:GetTall() - 60 - 5) 193 | 194 | self.TimelinesPanel:SetPos(248, 30) 195 | self.TimelinesPanel:SetSize(self:GetWide() - 456, self:GetTall() - 4 - 30) 196 | 197 | self.SelectedEntityLabel:SetPos(4, 5) 198 | self.SelectedEntityLabel:SetSize(self.TimelinesPanel:GetWide() - 8, 15) 199 | 200 | self.SettingPicker:SetPos(4, 25) 201 | self.SettingPicker:SetSize(self.TimelinesPanel:GetWide() - 10 - 25, 20) 202 | 203 | self.AddSettingPresetButton:SetPos(self.TimelinesPanel:GetWide() - 8 - 20, 25) 204 | self.AddSettingPresetButton:SetSize(20, 20) 205 | 206 | self.TimelinesCList:SetPos(5, 70) 207 | self.TimelinesCList:SetSize(self.TimelinesPanel:GetWide() - 10, self.TimelinesPanel:GetTall() - 70 - 5) 208 | 209 | self.AddTimeButton:SetPos(5, 50) 210 | self.AddTimeButton:SetSize(self.TimelinesCList:GetWide() / 2 - 2, 20) 211 | 212 | self.RemoveTimeButton:SetPos(5 + self.TimelinesCList:GetWide() / 2 + 2, 50) 213 | self.RemoveTimeButton:SetSize(self.TimelinesCList:GetWide() / 2 - 2, 20) 214 | 215 | self.ColorPanel:SetPos(248 + self:GetWide() - 456 + 4, 30) 216 | self.ColorPanel:SetSize(200, self:GetTall() - 4 - 30) 217 | 218 | self.ColorLabel:SetPos(4, 5) 219 | self.ColorLabel:SetSize(self.ColorPanel:GetWide() - 8, 15) 220 | 221 | self.ColorPicker:SetPos(5, 35) 222 | self.ColorPicker:SetSize(190, 130) 223 | 224 | self.ColorPreview:SetPos(5, 165 + 5) 225 | self.ColorPreview:SetSize(105, 15) 226 | 227 | self.SelectWorldButton:SetPos(5, 185 + 10) 228 | self.SelectWorldButton:SetSize(self.ColorPanel:GetWide() - 10, 20) 229 | 230 | self.WorldParent:SetPos(0, 225) 231 | self.WorldParent:SetSize(self.ColorPanel:GetWide(), self.ColorPanel:GetTall() - 225) 232 | 233 | self.ConsoleEnter:SetPos(5, 15) 234 | self.ConsoleEnter:SetSize(self.WorldParent:GetWide() - 10, 20) 235 | self.ConsoleEnter.Label:SetRelativePos(self.ConsoleEnter, 2, -5 - self.ConsoleEnter.Label:GetTall()) 236 | 237 | self.ButtonPressEnter:SetPos(5, 55) 238 | self.ButtonPressEnter:SetSize(self.WorldParent:GetWide() - 10, 20) 239 | self.ButtonPressEnter.Label:SetRelativePos(self.ButtonPressEnter, 2, -5 - self.ButtonPressEnter.Label:GetTall()) 240 | 241 | self.ButtonReleaseEnter:SetPos(5, 95) 242 | self.ButtonReleaseEnter:SetSize(self.WorldParent:GetWide() - 10, 20) 243 | self.ButtonReleaseEnter.Label:SetRelativePos(self.ButtonReleaseEnter, 2, -5 - self.ButtonReleaseEnter.Label:GetTall()) 244 | 245 | end 246 | 247 | function PANEL:MakeSettingSavePanel() 248 | if IsSaving then return end 249 | 250 | IsSaving = true 251 | 252 | local savepanel = vgui.Create("DFrame") 253 | savepanel:SetTitle("Save Timeline Preset") 254 | savepanel:SetPos((ScrW() / 2) - 100, (ScrH() / 2) - 50) 255 | savepanel:SetSize(200, 100) 256 | savepanel:MakePopup() 257 | savepanel:DoModal() 258 | savepanel:SetBackgroundBlur(true) 259 | 260 | savepanel.TextEnter = vgui.Create("DTextEntry", savepanel) 261 | savepanel.TextEnter:SetPos(50, 45) 262 | savepanel.TextEnter:SetSize(100, 20) 263 | 264 | savepanel.SaveButton = vgui.Create("DButton", savepanel) 265 | savepanel.SaveButton:SetPos(75, 75) 266 | savepanel.SaveButton:SetSize(50, 20) 267 | savepanel.SaveButton:SetText("Save") 268 | 269 | savepanel.SaveButton.DoClick = function() 270 | local text = savepanel.TextEnter:GetValue() 271 | if text == "" or text == "default" then return end 272 | self:SaveSettingsPreset(text) 273 | savepanel:Close() 274 | end 275 | 276 | savepanel.OnClose = function() 277 | savepanel:SetBackgroundBlur(false) 278 | IsSaving = false 279 | end 280 | end 281 | 282 | function PANEL:UpdateSelectedEnt(ent) 283 | local modelname = ent == LocalPlayer() and "world" or IsValid(ent) and ent:GetModel() or "none" 284 | self.SelectedEntityLabel:SetText("Selected model: " .. modelname) 285 | 286 | selectedEntity = ent 287 | self:SetEntities(EntsTable) 288 | end 289 | 290 | function PANEL:SetName(name) 291 | self.EntityNameEnter:SetText(name) 292 | UpdateName(name) 293 | self:SetEntities(EntsTable) 294 | end 295 | 296 | function PANEL:UpdateTimelineInfo(timelineinfo) 297 | PropertyTable = table.Copy(timelineinfo) 298 | self:BuildTimelineinfo() 299 | end 300 | 301 | function PANEL:UpdateModifiersInfo(timelineinfo, changed) 302 | PropertyTable = table.Copy(timelineinfo) 303 | 304 | for i = 1, PropertyTable.Timelines do 305 | local set = false 306 | for _, name in ipairs(PropertyTable.TimelineMods[i]) do 307 | if changed == name then 308 | self.TimelinesUI[i].Contents.Checker[name]:SetChecked(true) 309 | set = true 310 | end 311 | end 312 | if not set then 313 | self.TimelinesUI[i].Contents.Checker[changed]:SetChecked(false) 314 | end 315 | end 316 | end 317 | 318 | function PANEL:BuildTimelineinfo() 319 | self.TimelinesCList:Clear() 320 | self.TimelinesUI = {} 321 | 322 | self.ColorLabel:SetText("Keyframe Color for timeline: " .. "none") 323 | self.ColorPicker.Timeline = -1 324 | if next(PropertyTable) == nil then return end 325 | 326 | for i = 1, PropertyTable.Timelines do 327 | self.TimelinesUI[i] = self.TimelinesCList:Add("Timeline " .. i) 328 | self.TimelinesUI[i]:SetTall(200) 329 | self.TimelinesUI[i].Contents = vgui.Create("DPanel") 330 | self.TimelinesUI[i].OnToggle = function(_, expanded) 331 | if expanded then 332 | self.ColorLabel:SetText("Keyframe Color for timeline: " .. i) 333 | self.ColorPicker.Timeline = i 334 | self.ColorPicker:SetColor(PropertyTable.TimelineMods[i].KeyColor) 335 | end 336 | end 337 | self.TimelinesUI[i].Contents.Checker = {} 338 | for mod, name in pairs(ModifierList) do 339 | self.TimelinesUI[i].Contents.Checker[mod] = vgui.Create("DCheckBoxLabel", self.TimelinesUI[i].Contents) 340 | self.TimelinesUI[i].Contents.Checker[mod]:SetText(name) 341 | self.TimelinesUI[i].Contents.Checker[mod]:SetTextColor(Color(25, 25, 25)) 342 | self.TimelinesUI[i].Contents.Checker[mod].OnChange = function(_, check) 343 | if UsingWorld then return end 344 | self:OnUpdateModifierRequested(i, mod, check) 345 | end 346 | self.TimelinesUI[i].Contents.Checker[mod]:DockMargin(0, 0, 0, 2) 347 | self.TimelinesUI[i].Contents.Checker[mod]:Dock(TOP) 348 | end 349 | 350 | for _, mod in ipairs(PropertyTable.TimelineMods[i]) do 351 | self.TimelinesUI[i].Contents.Checker[mod]:SetChecked(true) 352 | end 353 | 354 | self.TimelinesUI[i]:SetContents(self.TimelinesUI[i].Contents) 355 | self.TimelinesUI[i]:SetExpanded(false) 356 | end 357 | end 358 | 359 | function PANEL:GetCurrentModifiers() 360 | if not PropertyTable or not PropertyTable.TimelineMods then return {} end 361 | return PropertyTable.TimelineMods[SMH.State.Timeline] 362 | end 363 | 364 | function PANEL:UpdateTimelineSettings() 365 | self.SettingPicker:Clear() 366 | self.SettingPicker:AddChoice("default") 367 | 368 | for _, setting in ipairs(SMH.Saves.ListSettings()) do 369 | self.SettingPicker:AddChoice(setting) 370 | end 371 | 372 | if ConVarExists("smh_currentpreset") then 373 | self.SettingPicker:SetValue(GetConVar("smh_currentpreset"):GetString()) 374 | else 375 | self.SettingPicker:SetValue("default") 376 | end 377 | end 378 | 379 | function PANEL:SetEntities(entities) 380 | local entlist = {} 381 | EntsTable = table.Copy(entities) 382 | 383 | if not IsValid(selectedEntity) then 384 | self.EntityNameEnter:SetText("none") 385 | self.EntityNameEnter:SetEditable(false) 386 | else 387 | local entityinfo = FindEntityInfo(selectedEntity) 388 | 389 | if not entityinfo then 390 | Fallback = GetModelName(selectedEntity) 391 | self.EntityNameEnter:SetText(Fallback) 392 | self.EntityNameEnter:SetEditable(false) 393 | else 394 | Fallback = entityinfo.Name 395 | self.EntityNameEnter:SetText(entityinfo.Name) 396 | self.EntityNameEnter:SetEditable(true) 397 | end 398 | end 399 | 400 | for entity, value in pairs(EntsTable) do 401 | table.insert(entlist, value.Name) 402 | end 403 | 404 | self.EntityList:UpdateLines(entlist) 405 | end 406 | 407 | function PANEL:InitModifiers(list) 408 | ModifierList = table.Copy(list) 409 | end 410 | 411 | function PANEL:GetModifiers() 412 | return ModifierList 413 | end 414 | 415 | function PANEL:SetUsingWorld(set) 416 | UsingWorld = set 417 | end 418 | 419 | function PANEL:GetUsingWorld() 420 | return UsingWorld 421 | end 422 | 423 | function PANEL:UpdateColor(timelineinfo) 424 | PropertyTable = table.Copy(timelineinfo) 425 | end 426 | 427 | function PANEL:ButtonTimeline(add) 428 | if next(PropertyTable) == nil then return end 429 | 430 | if add and PropertyTable.Timelines < 10 then 431 | self:OnAddTimelineRequested() 432 | 433 | elseif add then return 434 | 435 | elseif PropertyTable.Timelines > 1 then 436 | self:OnRemoveTimelineRequested() 437 | end 438 | end 439 | 440 | function PANEL:ShowWorldSettings(console, push, release) 441 | self.ConsoleEnter:SetValue(console) 442 | self.ButtonPressEnter:SetValue(push) 443 | self.ButtonReleaseEnter:SetValue(release) 444 | self.WorldParent:SetVisible(true) 445 | end 446 | 447 | function PANEL:HideWorldSettings() 448 | self.WorldParent:SetVisible(false) 449 | end 450 | 451 | function PANEL:InitTimelineSettings() 452 | local value 453 | 454 | if ConVarExists("smh_currentpreset") then 455 | value = GetConVar("smh_currentpreset"):GetString() 456 | else 457 | value = "default" 458 | end 459 | 460 | local settings = SMH.Saves.GetPreferences(value) 461 | if not settings then value = "default" end 462 | self:SetSettings(settings, value) 463 | end 464 | 465 | function PANEL:ApplyName(ent, name) end 466 | function PANEL:SelectEntity(entity) end 467 | function PANEL:SelectWorld() end 468 | function PANEL:OnAddTimelineRequested() end 469 | function PANEL:OnRemoveTimelineRequested() end 470 | function PANEL:OnUpdateModifierRequested(i, mod, check) end 471 | function PANEL:OnUpdateKeyframeColorRequested(color, timeline) end 472 | function PANEL:SetData(str, key) end 473 | function PANEL:SetSettings(settings, presetname) end 474 | function PANEL:SaveSettingsPreset(name) end 475 | 476 | vgui.Register("SMHProperties", PANEL, "DFrame") 477 | -------------------------------------------------------------------------------- /lua/smh/client/derma/save.lua: -------------------------------------------------------------------------------- 1 | local OverwriteWarningActive = false 2 | local AppendWindowActive = false 3 | local DeletePromptActive = false 4 | local FolderSelected = false 5 | 6 | local PANEL = {} 7 | 8 | function PANEL:Init() 9 | 10 | self:SetTitle("Save") 11 | self:SetDeleteOnClose(false) 12 | self:SetSizable(true) 13 | 14 | self:SetSize(250, 250) 15 | self:SetMinWidth(250) 16 | self:SetMinHeight(250) 17 | self:SetPos(ScrW() / 2 - self:GetWide() / 2, ScrH() / 2 - self:GetTall() / 2) 18 | 19 | self.FileName = vgui.Create("DTextEntry", self) 20 | self.FileName.Label = vgui.Create("DLabel", self) 21 | self.FileName.Label:SetText("Name") 22 | self.FileName.Label:SizeToContents() 23 | 24 | self.FileList = vgui.Create("DListView", self) 25 | self.FileList:SetMultiSelect(false) 26 | self.FileList:AddColumn("Saved scenes") 27 | self.FileList.OnRowSelected = function(_, rowID, row) 28 | if not IsValid(row) or row:GetValue(1) == ".." then 29 | return 30 | elseif row.IsFolder then 31 | self.FileName:SetValue(string.sub(row:GetValue(1), 2)) 32 | FolderSelected = true 33 | return 34 | end 35 | self.FileName:SetValue(row:GetValue(1)) 36 | FolderSelected = false 37 | end 38 | self.FileList.DoDoubleClick = function(_, rowID, row) 39 | if not IsValid(row) then 40 | return 41 | end 42 | local path = row:GetValue(1) 43 | if not (row.IsFolder or path == "..") then return end 44 | if row.IsFolder then path = string.sub(path, 2) end 45 | 46 | self:DoFolderPath(path) 47 | end 48 | 49 | self.PathLabel = vgui.Create("DLabel", self) 50 | self.PathLabel:SetMouseInputEnabled(true) 51 | self.PathLabel:SetText("smh/") 52 | self.PathLabel:SetTooltip("smh/") 53 | 54 | self.Save = vgui.Create("DButton", self) 55 | self.Save:SetText("Save") 56 | self.Save.DoClick = function() 57 | self:DoSave() 58 | end 59 | 60 | self.MakeFolder = vgui.Create("DButton", self) 61 | self.MakeFolder:SetText("Add Folder") 62 | self.MakeFolder.DoClick = function() 63 | self:DoFolder() 64 | end 65 | 66 | self.Pack = vgui.Create("DButton", self) 67 | self.Pack:SetText("Pack") 68 | self.Pack.DoClick = function() 69 | self:OnPackRequested() 70 | end 71 | 72 | self.Delete = vgui.Create("DButton", self) 73 | self.Delete:SetText("Delete") 74 | self.Delete.DoClick = function() 75 | self:DoDelete() 76 | end 77 | 78 | end 79 | 80 | function PANEL:PerformLayout(width, height) 81 | 82 | self.BaseClass.PerformLayout(self, width, height) 83 | 84 | local xOffset, yOffset = (self:GetWide()*0.2 - 50), (self:GetTall()*0.1 - 25) 85 | 86 | self.FileName:SetPos(5, 45) 87 | self.FileName:SetSize(self:GetWide() - 75 - xOffset, 20) 88 | self.FileName.Label:SetPos(5, 30) 89 | 90 | self.FileList:SetPos(5, 67) 91 | self.FileList:SetSize(self:GetWide() - 75 - xOffset, 153 + (self:GetTall() - 250)) 92 | 93 | self.PathLabel:SetPos(5, 220 + (self:GetTall() - 250)) 94 | self.PathLabel:SetSize(self:GetWide() - 15 - 60, 20) 95 | 96 | self.Save:SetPos(self:GetWide() - 65 - xOffset, 67) 97 | self.Save:SetSize(60 + xOffset, 20 + yOffset) 98 | 99 | self.MakeFolder:SetPos(self:GetWide() - 65 - xOffset, 97 + 2*yOffset) 100 | self.MakeFolder:SetSize(60 + xOffset, 20 + yOffset) 101 | 102 | self.Pack:SetPos(self:GetWide() - 65 - xOffset, 127 + 4*yOffset) 103 | self.Pack:SetSize(60 + xOffset, 20 + yOffset) 104 | 105 | self.Delete:SetPos(self:GetWide() - 65 - xOffset, 207 + 6*yOffset) 106 | self.Delete:SetSize(60 + xOffset, 20 + yOffset) 107 | 108 | end 109 | 110 | function PANEL:SetSaves(folders, saves, path) 111 | self.FileList:UpdateLines(folders, true) 112 | self.FileList:UpdateLines(saves) 113 | self.PathLabel:SetText(path) 114 | self.PathLabel:SetTooltip(path) 115 | 116 | local kablooey = string.Explode("/", path) 117 | if #kablooey > 2 then 118 | local line = self.FileList:AddLine("..") 119 | self.FileList:SortByColumn(1) 120 | end 121 | end 122 | 123 | function PANEL:AddSave(path) 124 | self.FileList:AddLine(path) 125 | end 126 | 127 | function PANEL:AddFolder(path) 128 | local line = self.FileList:AddLine(path) 129 | line.IsFolder = true 130 | end 131 | 132 | function PANEL:RemoveSave(path, isFolder) 133 | if isFolder then path = "\\" .. path end 134 | 135 | for idx, line in pairs(self.FileList:GetLines()) do 136 | if line:GetValue(1) == path then 137 | self.FileList:RemoveLine(idx) 138 | break 139 | end 140 | end 141 | end 142 | 143 | function PANEL:DoSave() 144 | local path = self.FileName:GetValue() 145 | if not path or path == "" then 146 | return 147 | end 148 | 149 | FolderSelected = false 150 | -- TODO clientside support for loading and saving 151 | self:OnSaveRequested(path, false) 152 | end 153 | 154 | function PANEL:DoFolder() 155 | local path = self.FileName:GetValue() 156 | if not path or path == "" then 157 | return 158 | end 159 | 160 | FolderSelected = true 161 | -- TODO clientside support for loading and saving 162 | self:OnFolderRequested(path, false) 163 | end 164 | 165 | function PANEL:DoFolderPath(path) 166 | if not path or path == "" then 167 | return 168 | end 169 | 170 | self:OnGoToFolderRequested(path) 171 | end 172 | 173 | function PANEL:DoDelete() 174 | if DeletePromptActive then return end 175 | 176 | local path = self.FileName:GetValue() 177 | if not path or path == "" then 178 | return 179 | end 180 | 181 | DeletePromptActive = true 182 | 183 | local promptpanel = vgui.Create("DFrame") 184 | promptpanel:SetTitle("Confirm delete") 185 | promptpanel:SetPos((ScrW() / 2) - 250/2, (ScrH() / 2) - 200/2) 186 | promptpanel:SetSize(250, 200) 187 | promptpanel:MakePopup() 188 | promptpanel:DoModal() 189 | promptpanel:SetBackgroundBlur(true) 190 | 191 | local text = 'Are you sure you want to delete "' .. path .. '"?' 192 | if FolderSelected then 193 | text = text .. "\n\nCaution: you can't delete folders that still have files in them" 194 | end 195 | 196 | promptpanel.Label = vgui.Create("DLabel", promptpanel) 197 | promptpanel.Label:SetPos(25, 30) 198 | promptpanel.Label:SetSize(200, 20) 199 | promptpanel.Label:SetText(text) 200 | promptpanel.Label:SetWrap(true) 201 | promptpanel.Label:SetAutoStretchVertical(true) 202 | 203 | promptpanel.Delete = vgui.Create("DButton", promptpanel) 204 | promptpanel.Delete:SetPos(45, 175) 205 | promptpanel.Delete:SetSize(60, 20) 206 | promptpanel.Delete:SetText("Delete") 207 | 208 | promptpanel.Delete.DoClick = function() 209 | self:OnDeleteRequested(path, FolderSelected) 210 | FolderSelected = false 211 | promptpanel:Close() 212 | end 213 | 214 | promptpanel.Cancel = vgui.Create("DButton", promptpanel) 215 | promptpanel.Cancel:SetPos(145, 175) 216 | promptpanel.Cancel:SetSize(60, 20) 217 | promptpanel.Cancel:SetText("Cancel") 218 | 219 | promptpanel.Cancel.DoClick = function() 220 | promptpanel:Close() 221 | end 222 | 223 | promptpanel.OnClose = function() 224 | promptpanel:SetBackgroundBlur(false) 225 | DeletePromptActive = false 226 | end 227 | end 228 | 229 | function PANEL:SaveExists(names) 230 | if OverwriteWarningActive then return end 231 | 232 | local path = self.FileName:GetValue() 233 | local namelist 234 | if not path or path == "" then 235 | return 236 | end 237 | 238 | OverwriteWarningActive = true 239 | 240 | local overwritepanel = vgui.Create("DFrame") 241 | overwritepanel:SetTitle("Overwrite save?") 242 | overwritepanel:SetPos((ScrW() / 2) - 250/2, (ScrH() / 2) - 250/2) 243 | overwritepanel:SetSize(250, 250) 244 | overwritepanel:MakePopup() 245 | overwritepanel:DoModal() 246 | overwritepanel:SetBackgroundBlur(true) 247 | 248 | overwritepanel.ScrollPanel = vgui.Create("DScrollPanel", overwritepanel) 249 | overwritepanel.ScrollPanel:SetPos(25, 30) 250 | overwritepanel.ScrollPanel:SetSize(220, 190) 251 | 252 | if next(names) ~= nil then 253 | namelist = {"Following animations from the save will be lost:\n"} 254 | for _, name in ipairs(names) do 255 | table.insert(namelist, "- " .. name .. "\n") 256 | end 257 | namelist = table.concat(namelist) 258 | else 259 | namelist = "All animations from the save will be overridden" 260 | end 261 | 262 | overwritepanel.ScrollPanel.Text = vgui.Create("DLabel") 263 | overwritepanel.ScrollPanel.Text:SetSize(200, 20) 264 | overwritepanel.ScrollPanel.Text:SetText('Save "' .. path .. '" already exists. Do you want to replace it?\n\n' .. namelist .. "\n\nUse Append mode to merge animations from the game session into the save.") 265 | overwritepanel.ScrollPanel.Text:SetWrap(true) 266 | overwritepanel.ScrollPanel.Text:SetAutoStretchVertical(true) 267 | 268 | overwritepanel.ScrollPanel:AddItem(overwritepanel.ScrollPanel.Text) 269 | 270 | overwritepanel.AppendButton = vgui.Create("DButton", overwritepanel) 271 | overwritepanel.AppendButton:SetPos(25, 225) 272 | overwritepanel.AppendButton:SetSize(60, 20) 273 | overwritepanel.AppendButton:SetText("Append") 274 | 275 | overwritepanel.AppendButton.DoClick = function() 276 | self:OnAppendRequested(path) 277 | overwritepanel:Close() 278 | end 279 | 280 | overwritepanel.WriteButton = vgui.Create("DButton", overwritepanel) 281 | overwritepanel.WriteButton:SetPos(95, 225) 282 | overwritepanel.WriteButton:SetSize(60, 20) 283 | overwritepanel.WriteButton:SetText("Overwrite") 284 | 285 | overwritepanel.WriteButton.DoClick = function() 286 | self:OnOverwriteSave(path) 287 | overwritepanel:Close() 288 | end 289 | 290 | overwritepanel.CloseButton = vgui.Create("DButton", overwritepanel) 291 | overwritepanel.CloseButton:SetPos(165, 225) 292 | overwritepanel.CloseButton:SetSize(60, 20) 293 | overwritepanel.CloseButton:SetText("Cancel") 294 | 295 | overwritepanel.CloseButton.DoClick = function() 296 | overwritepanel:Close() 297 | end 298 | 299 | overwritepanel.OnClose = function() 300 | overwritepanel:SetBackgroundBlur(false) 301 | OverwriteWarningActive = false 302 | end 303 | end 304 | 305 | function PANEL:AppendWindow(savenames, gamenames) 306 | if AppendWindowActive then return end 307 | 308 | local path = self.FileName:GetValue() 309 | if not path or path == "" then 310 | return 311 | end 312 | 313 | AppendWindowActive = true 314 | 315 | local appendpanel = vgui.Create("DFrame") 316 | appendpanel:SetTitle("Append") 317 | appendpanel:SetPos((ScrW() / 2) - 300/2, (ScrH() / 2) - 350/2) 318 | appendpanel:SetSize(300, 350) 319 | appendpanel:MakePopup() 320 | appendpanel:DoModal() 321 | appendpanel:SetBackgroundBlur(true) 322 | 323 | appendpanel.ScrollPanel = vgui.Create("DScrollPanel", appendpanel) 324 | appendpanel.ScrollPanel:SetPos(25, 30) 325 | appendpanel.ScrollPanel:SetSize(270, 290) 326 | 327 | appendpanel.ScrollPanel.NameLeft = vgui.Create("DLabel", appendpanel.ScrollPanel) 328 | appendpanel.ScrollPanel.NameLeft:SetSize(100, 20) 329 | appendpanel.ScrollPanel.NameLeft:SetPos(0, 5) 330 | appendpanel.ScrollPanel.NameLeft:SetText("To be saved:") 331 | 332 | appendpanel.ScrollPanel.NameRight = vgui.Create("DLabel", appendpanel.ScrollPanel) 333 | appendpanel.ScrollPanel.NameRight:SetSize(100, 20) 334 | appendpanel.ScrollPanel.NameRight:SetPos(130, 5) 335 | appendpanel.ScrollPanel.NameRight:SetText("In Save:") 336 | 337 | 338 | local savestuff, gamestuff = {}, {} 339 | appendpanel.ScrollPanel.SaveNames = {} 340 | appendpanel.ScrollPanel.GameNames = {} 341 | 342 | for id, name in ipairs(savenames) do 343 | appendpanel.ScrollPanel.SaveNames[id] = vgui.Create("DCheckBox", appendpanel.ScrollPanel) 344 | appendpanel.ScrollPanel.SaveNames[id]:SetSize(15, 15) 345 | appendpanel.ScrollPanel.SaveNames[id]:SetPos(130, 30 + (id - 1)*20) 346 | appendpanel.ScrollPanel.SaveNames[id].Label = vgui.Create("DLabel", appendpanel.ScrollPanel) 347 | appendpanel.ScrollPanel.SaveNames[id].Label:SetSize(80, 15) 348 | appendpanel.ScrollPanel.SaveNames[id].Label:SetPos(130 + 20, 30 + (id - 1)*20) 349 | appendpanel.ScrollPanel.SaveNames[id].Label:SetMouseInputEnabled(true) 350 | appendpanel.ScrollPanel.SaveNames[id].Label:SetText(name) 351 | appendpanel.ScrollPanel.SaveNames[id].Label:SetTooltip(name) 352 | 353 | appendpanel.ScrollPanel.SaveNames[id].Label.DoClick = function() 354 | appendpanel.ScrollPanel.SaveNames[id]:Toggle() 355 | end 356 | 357 | appendpanel.ScrollPanel.SaveNames[id].OnChange = function(_, val) 358 | savestuff[name] = val 359 | end 360 | 361 | appendpanel.ScrollPanel.SaveNames[id]:SetValue(true) 362 | end 363 | 364 | for id, name in ipairs(gamenames) do 365 | appendpanel.ScrollPanel.GameNames[id] = vgui.Create("DCheckBox", appendpanel.ScrollPanel) 366 | appendpanel.ScrollPanel.GameNames[id]:SetSize(15, 15) 367 | appendpanel.ScrollPanel.GameNames[id]:SetPos(0, 30 + (id - 1)*20) 368 | appendpanel.ScrollPanel.GameNames[id].Label = vgui.Create("DLabel", appendpanel.ScrollPanel) 369 | appendpanel.ScrollPanel.GameNames[id].Label:SetSize(80, 15) 370 | appendpanel.ScrollPanel.GameNames[id].Label:SetPos(20, 30 + (id - 1)*20) 371 | appendpanel.ScrollPanel.GameNames[id].Label:SetMouseInputEnabled(true) 372 | appendpanel.ScrollPanel.GameNames[id].Label:SetText(name) 373 | appendpanel.ScrollPanel.GameNames[id].Label:SetTooltip(name) 374 | 375 | appendpanel.ScrollPanel.GameNames[id].Label.DoClick = function() 376 | appendpanel.ScrollPanel.GameNames[id]:Toggle() 377 | end 378 | 379 | appendpanel.ScrollPanel.GameNames[id].OnChange = function(_, val) 380 | gamestuff[name] = val 381 | end 382 | 383 | appendpanel.ScrollPanel.GameNames[id]:SetValue(true) 384 | end 385 | 386 | 387 | appendpanel.ConfirmButton = vgui.Create("DButton", appendpanel) 388 | appendpanel.ConfirmButton:SetPos(75, 325) 389 | appendpanel.ConfirmButton:SetSize(60, 20) 390 | appendpanel.ConfirmButton:SetText("Confirm") 391 | 392 | appendpanel.ConfirmButton.DoClick = function() 393 | local save, game = {}, {} 394 | 395 | for name, value in pairs(savestuff) do 396 | if value then 397 | table.insert(save, name) 398 | end 399 | end 400 | for name, value in pairs(gamestuff) do 401 | if value then 402 | table.insert(game, name) 403 | end 404 | end 405 | self:OnAppend(path, save, game) 406 | appendpanel:Close() 407 | end 408 | 409 | appendpanel.CloseButton = vgui.Create("DButton", appendpanel) 410 | appendpanel.CloseButton:SetPos(165, 325) 411 | appendpanel.CloseButton:SetSize(60, 20) 412 | appendpanel.CloseButton:SetText("Cancel") 413 | 414 | appendpanel.CloseButton.DoClick = function() 415 | appendpanel:Close() 416 | end 417 | 418 | appendpanel.OnClose = function() 419 | appendpanel:SetBackgroundBlur(false) 420 | AppendWindowActive = false 421 | end 422 | end 423 | 424 | function PANEL:OnSaveRequested(path, saveToClient) end 425 | function PANEL:OnFolderRequested(path, saveToClient) end 426 | function PANEL:OnGoToFolderRequested(path, toClient) end 427 | function PANEL:OnOverwriteSave(path) end 428 | function PANEL:OnAppendRequested(path) end 429 | function PANEL:OnAppend(path, savenames, gamenames) end 430 | function PANEL:OnPackRequested() end 431 | function PANEL:OnDeleteRequested(path, isFolder, deleteFromClient) end 432 | 433 | vgui.Register("SMHSave", PANEL, "DFrame") 434 | -------------------------------------------------------------------------------- /lua/smh/client/derma/settings.lua: -------------------------------------------------------------------------------- 1 | local PANEL = {} 2 | 3 | function PANEL:Init() 4 | 5 | local function CreateSettingChanger(name) 6 | return function(_self, value) 7 | if self._changingSettings then 8 | return 9 | end 10 | 11 | local updatedSettings = { 12 | [name] = value 13 | } 14 | self:OnSettingsUpdated(updatedSettings) 15 | end 16 | end 17 | 18 | local function CreateCheckBox(name, label) 19 | local cb = vgui.Create("DCheckBoxLabel", self) 20 | cb:SetText(label) 21 | cb:SizeToContents() 22 | cb.OnChange = CreateSettingChanger(name) 23 | return cb 24 | end 25 | 26 | local function CreateSlider(name, label, min, max, decimals) 27 | local slider = vgui.Create("DNumSlider", self) 28 | slider:SetMinMax(min, max) 29 | slider:SetDecimals(decimals) 30 | slider:SetText(label) 31 | slider.OnValueChanged = CreateSettingChanger(name) 32 | return slider 33 | end 34 | 35 | self:SetTitle("SMH Settings") 36 | self:SetDeleteOnClose(false) 37 | 38 | self.FreezeAll = CreateCheckBox("FreezeAll", "Freeze all") 39 | self.LocalizePhysBones = CreateCheckBox("LocalizePhysBones", "Localize phys bones") 40 | self.IgnorePhysBones = CreateCheckBox("IgnorePhysBones", "Don't animate phys bones") 41 | self.GhostPrevFrame = CreateCheckBox("GhostPrevFrame", "Ghost previous frame") 42 | self.GhostNextFrame = CreateCheckBox("GhostNextFrame", "Ghost next frame") 43 | self.GhostAllEntities = CreateCheckBox("GhostAllEntities", "Ghost all entities") 44 | self.TweenDisable = CreateCheckBox("TweenDisable", "Disable tweening") 45 | self.SmoothPlayback = CreateCheckBox("SmoothPlayback", "Smooth playback") 46 | self.EnableWorld = CreateCheckBox("EnableWorld", "Enable World keyframes") 47 | self.GhostTransparency = CreateSlider("GhostTransparency", "Ghost transparency", 0, 1, 2) 48 | 49 | self.PhysButton = vgui.Create("DButton", self) 50 | self.PhysButton:SetText("Physics Recorder") 51 | self.PhysButton.DoClick = function() 52 | self:OnRequestOpenPhysRecorder() 53 | end 54 | 55 | self.HelpButton = vgui.Create("DButton", self) 56 | self.HelpButton:SetText("Help") 57 | self.HelpButton.DoClick = function() 58 | self:OnRequestOpenHelp() 59 | end 60 | 61 | self:SetSize(250, 290) 62 | 63 | self._changingSettings = false 64 | 65 | end 66 | 67 | function PANEL:PerformLayout(width, height) 68 | 69 | self.BaseClass.PerformLayout(self, width, height) 70 | 71 | self.FreezeAll:SetPos(5, 25) 72 | 73 | self.LocalizePhysBones:SetPos(5, 45) 74 | 75 | self.IgnorePhysBones:SetPos(5, 65) 76 | 77 | self.GhostPrevFrame:SetPos(5, 85) 78 | self.GhostNextFrame:SetPos(5, 105) 79 | self.GhostAllEntities:SetPos(5, 125) 80 | 81 | self.TweenDisable:SetPos(5, 145) 82 | 83 | self.SmoothPlayback:SetPos(5, 165) 84 | 85 | self.EnableWorld:SetPos(5, 185) 86 | 87 | self.GhostTransparency:SetPos(5, 205) 88 | self.GhostTransparency:SetSize(self:GetWide() - 5 - 5, 25) 89 | 90 | self.PhysButton:SetPos(5, 230) 91 | self.PhysButton:SetSize(self:GetWide() - 10, 20) 92 | 93 | self.HelpButton:SetPos(5, 255) 94 | self.HelpButton:SetSize(self:GetWide() - 5 - 5, 20) 95 | 96 | end 97 | 98 | function PANEL:ApplySettings(settings) 99 | self._changingSettings = true 100 | 101 | local checkBoxes = { 102 | "FreezeAll", 103 | "LocalizePhysBones", 104 | "IgnorePhysBones", 105 | "GhostPrevFrame", 106 | "GhostNextFrame", 107 | "GhostAllEntities", 108 | "TweenDisable", 109 | "SmoothPlayback", 110 | "EnableWorld", 111 | } 112 | 113 | for _, key in pairs(checkBoxes) do 114 | if settings[key] ~= nil then 115 | self[key]:SetChecked(settings[key]) 116 | end 117 | end 118 | 119 | if settings.GhostTransparency ~= nil then 120 | self.GhostTransparency:SetValue(settings.GhostTransparency) 121 | end 122 | 123 | self._changingSettings = false 124 | end 125 | 126 | function PANEL:OnSettingsUpdated(settings) end 127 | function PANEL:OnRequestOpenHelp() end 128 | function PANEL:OnRequestOpenPhysRecorder() end 129 | 130 | vgui.Register("SMHSettings", PANEL, "DFrame") 131 | -------------------------------------------------------------------------------- /lua/smh/client/derma/smh_menu.lua: -------------------------------------------------------------------------------- 1 | local PANEL = {} 2 | 3 | function PANEL:Init() 4 | 5 | self:SetTitle("Stop Motion Helper") 6 | self:SetSize(ScrW(), 90) 7 | self:SetPos(0, ScrH() - self:GetTall()) 8 | self:SetDraggable(false) 9 | self:ShowCloseButton(false) 10 | self:SetDeleteOnClose(false) 11 | self:ShowCloseButton(false) 12 | 13 | self._sendKeyframeChanges = true 14 | 15 | self.FramePanel = vgui.Create("SMHFramePanel", self) 16 | 17 | self.FramePointer = self.FramePanel:CreateFramePointer(Color(255, 255, 255), self.FramePanel:GetTall() / 4, true) 18 | 19 | self.TimelinesBase = vgui.Create("Panel", self) 20 | 21 | self.PositionLabel = vgui.Create("DLabel", self) 22 | 23 | self.PlaybackRateControl = vgui.Create("DNumberWang", self) 24 | self.PlaybackRateControl:SetMinMax(1, 216000) 25 | self.PlaybackRateControl:SetDecimals(0) 26 | self.PlaybackRateControl.OnValueChanged = function(_, value) 27 | self:OnRequestStateUpdate({ PlaybackRate = tonumber(value) }) 28 | end 29 | self.PlaybackRateControl.Label = vgui.Create("DLabel", self) 30 | self.PlaybackRateControl.Label:SetText("Framerate") 31 | self.PlaybackRateControl.Label:SizeToContents() 32 | 33 | self.PlaybackLengthControl = vgui.Create("DNumberWang", self) 34 | self.PlaybackLengthControl:SetMinMax(1, 999) 35 | self.PlaybackLengthControl:SetDecimals(0) 36 | self.PlaybackLengthControl.OnValueChanged = function(_, value) 37 | self:OnRequestStateUpdate({ PlaybackLength = tonumber(value) }) 38 | end 39 | self.PlaybackLengthControl.Label = vgui.Create("DLabel", self) 40 | self.PlaybackLengthControl.Label:SetText("Frame count") 41 | self.PlaybackLengthControl.Label:SizeToContents() 42 | 43 | self.Easing = vgui.Create("Panel", self) 44 | 45 | self.EaseInControl = vgui.Create("DNumberWang", self.Easing) 46 | self.EaseInControl:SetNumberStep(0.1) 47 | self.EaseInControl:SetMinMax(0, 1) 48 | self.EaseInControl:SetDecimals(1) 49 | self.EaseInControl.OnValueChanged = function(_, value) 50 | if self._sendKeyframeChanges then 51 | self:OnRequestKeyframeUpdate({ EaseIn = tonumber(value) }) 52 | end 53 | end 54 | self.EaseInControl.Label = vgui.Create("DLabel", self.Easing) 55 | self.EaseInControl.Label:SetText("Ease in") 56 | self.EaseInControl.Label:SizeToContents() 57 | 58 | self.EaseOutControl = vgui.Create("DNumberWang", self.Easing) 59 | self.EaseOutControl:SetNumberStep(0.1) 60 | self.EaseOutControl:SetMinMax(0, 1) 61 | self.EaseOutControl:SetDecimals(1) 62 | self.EaseOutControl.OnValueChanged = function(_, value) 63 | if self._sendKeyframeChanges then 64 | self:OnRequestKeyframeUpdate({ EaseOut = tonumber(value) }) 65 | end 66 | end 67 | self.EaseOutControl.Label = vgui.Create("DLabel", self.Easing) 68 | self.EaseOutControl.Label:SetText("Ease out") 69 | self.EaseOutControl.Label:SizeToContents() 70 | 71 | self.RecordButton = vgui.Create("DButton", self) 72 | self.RecordButton:SetText("Record") 73 | self.RecordButton.DoClick = function() self:OnRequestRecord() end 74 | 75 | self.PropertiesButton = vgui.Create("DButton", self) 76 | self.PropertiesButton:SetText("Properties") 77 | self.PropertiesButton.DoClick = function() self:OnRequestOpenPropertiesMenu() end 78 | 79 | self.SaveButton = vgui.Create("DButton", self) 80 | self.SaveButton:SetText("Save") 81 | self.SaveButton.DoClick = function() self:OnRequestOpenSaveMenu() end 82 | 83 | self.LoadButton = vgui.Create("DButton", self) 84 | self.LoadButton:SetText("Load") 85 | self.LoadButton.DoClick = function() self:OnRequestOpenLoadMenu() end 86 | 87 | self.SettingsButton = vgui.Create("DButton", self) 88 | self.SettingsButton:SetText("Settings") 89 | self.SettingsButton.DoClick = function() self:OnRequestOpenSettings() end 90 | 91 | self.Easing:SetVisible(false) 92 | 93 | end 94 | 95 | function PANEL:PerformLayout(width, height) 96 | 97 | self.BaseClass.PerformLayout(self, width, height) 98 | 99 | self:SetTitle("Stop Motion Helper") 100 | 101 | self.FramePanel:SetPos(5, 40) 102 | self.FramePanel:SetSize(width - 5 * 2, 45) 103 | 104 | self.FramePointer.VerticalPosition = self.FramePanel:GetTall() / 4 105 | 106 | self.TimelinesBase:SetPos(0, 25) 107 | self.TimelinesBase:SetSize(ScrW(),15) 108 | 109 | self.PositionLabel:SetPos(150, 5) 110 | 111 | self.PlaybackRateControl:SetPos(340, 2) 112 | self.PlaybackRateControl:SetSize(50, 20) 113 | local sizeX, sizeY = self.PlaybackRateControl.Label:GetSize() 114 | self.PlaybackRateControl.Label:SetRelativePos(self.PlaybackRateControl, -(sizeX) - 5, 3) 115 | 116 | self.PlaybackLengthControl:SetPos(460, 2) 117 | self.PlaybackLengthControl:SetSize(50, 20) 118 | sizeX, sizeY = self.PlaybackLengthControl.Label:GetSize() 119 | self.PlaybackLengthControl.Label:SetRelativePos(self.PlaybackLengthControl, -(sizeX) - 5, 3) 120 | 121 | self.Easing:SetPos(540, 0) 122 | self.Easing:SetSize(250, 30) 123 | 124 | self.EaseInControl:SetPos(60, 2) 125 | self.EaseInControl:SetSize(50, 20) 126 | sizeX, sizeY = self.EaseInControl.Label:GetSize() 127 | self.EaseInControl.Label:SetRelativePos(self.EaseInControl, -(sizeX) - 5, 3) 128 | 129 | self.EaseOutControl:SetPos(160, 2) 130 | self.EaseOutControl:SetSize(50, 20) 131 | sizeX, sizeY = self.EaseOutControl.Label:GetSize() 132 | self.EaseOutControl.Label:SetRelativePos(self.EaseOutControl, -(sizeX) - 5, 3) 133 | 134 | self.RecordButton:SetPos(width - 60 * 5 - 5 * 5, 2) 135 | self.RecordButton:SetSize(60, 20) 136 | 137 | self.PropertiesButton:SetPos(width - 60 * 4 - 5 * 4, 2) 138 | self.PropertiesButton:SetSize(60, 20) 139 | 140 | self.SaveButton:SetPos(width - 60 * 3 - 5 * 3, 2) 141 | self.SaveButton:SetSize(60, 20) 142 | 143 | self.LoadButton:SetPos(width - 60 * 2 - 5 * 2, 2) 144 | self.LoadButton:SetSize(60, 20) 145 | 146 | self.SettingsButton:SetPos(width - 60 * 1 - 5 * 1, 2) 147 | self.SettingsButton:SetSize(60, 20) 148 | 149 | end 150 | 151 | function PANEL:UpdateTimelines(timelineinfo) 152 | self.TimelinesBase:Clear() 153 | 154 | if next(timelineinfo) == nil then return end --check if supplied table is empty 155 | local TotallTimelines = timelineinfo.Timelines 156 | if TotallTimelines < SMH.State.Timeline then SMH.State.Timeline = 1 end 157 | self.TimelinesBase.Timeline = {} 158 | 159 | for i = 1, TotallTimelines do 160 | self.TimelinesBase.Timeline[i] = vgui.Create("DPanel", self.TimelinesBase) 161 | self.TimelinesBase.Timeline[i]:SetPos((i - 1) * (ScrW() / TotallTimelines) + 4,0) 162 | self.TimelinesBase.Timeline[i]:SetSize((ScrW() / TotallTimelines) - 8,15) 163 | if i == SMH.State.Timeline then 164 | self.TimelinesBase.Timeline[i]:SetBackgroundColor(Color(220, 220, 220, 255)) 165 | else 166 | self.TimelinesBase.Timeline[i]:SetBackgroundColor(Color(175, 175, 175, 255)) 167 | end 168 | 169 | self.TimelinesBase.Timeline[i].Label = vgui.Create("DLabel", self.TimelinesBase.Timeline[i]) 170 | self.TimelinesBase.Timeline[i].Label:SetText("Timeline " .. i) 171 | self.TimelinesBase.Timeline[i].Label:SetTextColor(Color(100, 100, 100)) 172 | self.TimelinesBase.Timeline[i].Label:SizeToContents() 173 | self.TimelinesBase.Timeline[i].Label:Center() 174 | 175 | self.TimelinesBase.Timeline[i]._pressed = false 176 | self.TimelinesBase.Timeline[i].OnMousePressed = function(_, mousecode) 177 | if mousecode ~= MOUSE_LEFT then return end 178 | 179 | SMH.State.Timeline = i 180 | SMH.Controller.UpdateTimeline() 181 | 182 | for j = 1, TotallTimelines do 183 | if j ~= i then 184 | self.TimelinesBase.Timeline[j]:SetBackgroundColor(Color(175, 175, 175, 255)) 185 | else 186 | self.TimelinesBase.Timeline[j]:SetBackgroundColor(Color(220, 220, 220, 255)) 187 | end 188 | end 189 | end 190 | end 191 | end 192 | 193 | function PANEL:SetInitialState(state) 194 | self.PlaybackRateControl:SetValue(state.PlaybackRate) 195 | self.PlaybackLengthControl:SetValue(state.PlaybackLength) 196 | self:UpdatePositionLabel(state.Frame, state.PlaybackLength) 197 | end 198 | 199 | function PANEL:UpdatePositionLabel(frame, totalFrames) 200 | local offset = GetConVar("smh_startatone"):GetInt() 201 | self.PositionLabel:SetText("Position: " .. frame + offset .. " / " .. totalFrames - (1 - offset)) 202 | self.PositionLabel:SizeToContents() 203 | end 204 | 205 | function PANEL:ShowEasingControls(easeIn, easeOut) 206 | self._sendKeyframeChanges = false 207 | self.EaseInControl:SetValue(easeIn) 208 | self.EaseOutControl:SetValue(easeOut) 209 | self.Easing:SetVisible(true) 210 | self._sendKeyframeChanges = true 211 | end 212 | 213 | function PANEL:HideEasingControls() 214 | self.Easing:SetVisible(false) 215 | end 216 | 217 | function PANEL:OnRequestStateUpdate(newState) end 218 | function PANEL:OnRequestKeyframeUpdate(newKeyframeData) end 219 | function PANEL:OnRequestOpenPropertiesMenu() end 220 | function PANEL:OnRequestRecord() end 221 | function PANEL:OnRequestOpenSaveMenu() end 222 | function PANEL:OnRequestOpenLoadMenu() end 223 | function PANEL:OnRequestOpenSettings() end 224 | 225 | vgui.Register("SMHMenu", PANEL, "DFrame") 226 | -------------------------------------------------------------------------------- /lua/smh/client/derma/spawn.lua: -------------------------------------------------------------------------------- 1 | local SaveFile = nil 2 | 3 | local PANEL = {} 4 | 5 | function PANEL:Init() 6 | 7 | local function CreateSlider(label, min, max, func) 8 | local slider = vgui.Create("DNumSlider", self) 9 | slider:SetMinMax(min, max) 10 | slider:SetDecimals(1) 11 | slider:SetDefaultValue(0) 12 | slider:SetValue(0) 13 | slider:SetText(label) 14 | slider.OnValueChanged = func 15 | return slider 16 | end 17 | 18 | self:SetTitle("Spawn Menu") 19 | self:SetDeleteOnClose(false) 20 | 21 | self:SetSize(300, 405) 22 | 23 | self.Origins = vgui.Create("DListView", self) 24 | self.Origins:AddColumn("Offset Origin") 25 | self.Origins:SetMultiSelect(false) 26 | self.Origins.OnRowSelected = function(_, rowIndex, row) 27 | if not SaveFile then return end 28 | self:OnOriginRequested(SaveFile,row:GetValue(1), false) 29 | end 30 | 31 | self.EntityList = vgui.Create("DListView", self) 32 | self.EntityList:AddColumn("Entities") 33 | self.EntityList:SetMultiSelect(false) 34 | self.EntityList.OnRowSelected = function(_, rowIndex, row) 35 | if not SaveFile then return end 36 | self:OnModelRequested(SaveFile,row:GetValue(1), false) 37 | end 38 | 39 | self.PositionLabel = vgui.Create("DLabel", self) 40 | self.PositionLabel:SetText("Position offset") 41 | self.PositionLabel:SizeToContents() 42 | 43 | self.XSlide = CreateSlider("X", -1000, 1000, function(_, value) 44 | local Pos = Vector(value, self.YSlide:GetValue(), self.ZSlide:GetValue()) 45 | SMH.Controller.OffsetPos(Pos) 46 | end) 47 | 48 | self.YSlide = CreateSlider("Y", -1000, 1000, function(_, value) 49 | local Pos = Vector(self.XSlide:GetValue(), value, self.ZSlide:GetValue()) 50 | SMH.Controller.OffsetPos(Pos) 51 | end) 52 | 53 | self.ZSlide = CreateSlider("Z", -1000, 1000, function(_, value) 54 | local Pos = Vector(self.XSlide:GetValue(), self.YSlide:GetValue(), value) 55 | SMH.Controller.OffsetPos(Pos) 56 | end) 57 | 58 | self.AngleLabel = vgui.Create("DLabel", self) 59 | self.AngleLabel:SetText("Angle offset") 60 | self.AngleLabel:SizeToContents() 61 | 62 | self.PSlide = CreateSlider("Pitch", -180, 180, function(_, value) 63 | local Ang = Angle(value, self.YawSlide:GetValue(), self.RSlide:GetValue()) 64 | SMH.Controller.OffsetAng(Ang) 65 | end) 66 | 67 | self.YawSlide = CreateSlider("Yaw", -180, 180, function(_, value) 68 | local Ang = Angle(self.PSlide:GetValue(), value, self.RSlide:GetValue()) 69 | SMH.Controller.OffsetAng(Ang) 70 | end) 71 | 72 | self.RSlide = CreateSlider("Roll", -180, 180, function(_, value) 73 | local Ang = Angle(self.PSlide:GetValue(), self.YawSlide:GetValue(), value) 74 | SMH.Controller.OffsetAng(Ang) 75 | end) 76 | 77 | self.OffsetCheck = vgui.Create("DCheckBoxLabel", self) 78 | self.OffsetCheck:SetText("Move to where you're looking") 79 | self.OffsetCheck.OnChange = function(_, value) 80 | self:SetOffsetMode(value) 81 | end 82 | 83 | self.Spawn = vgui.Create("DButton", self) 84 | self.Spawn:SetText("Spawn") 85 | self.Spawn.DoClick = function() 86 | self:SpawnSelected() 87 | end 88 | 89 | end 90 | 91 | function PANEL:PerformLayout(width, height) 92 | 93 | self.BaseClass.PerformLayout(self, width, height) 94 | 95 | self.Origins:SetPos(5, 30) 96 | self.Origins:SetSize(150 - 5 - 5, 150) 97 | 98 | self.EntityList:SetPos(150 + 5, 30) 99 | self.EntityList:SetSize(150 - 5 - 5, 150) 100 | 101 | self.PositionLabel:SetPos(5, 185) 102 | 103 | self.XSlide:SetPos(5, 200) 104 | self.XSlide:SetSize(300, 30) 105 | 106 | self.YSlide:SetPos(5, 200 + 15 + 5) 107 | self.YSlide:SetSize(300, 30) 108 | 109 | self.ZSlide:SetPos(5, 200 + 30 + 10) 110 | self.ZSlide:SetSize(300, 30) 111 | 112 | self.AngleLabel:SetPos(5, 270) 113 | 114 | self.PSlide:SetPos(5, 285) 115 | self.PSlide:SetSize(300, 30) 116 | 117 | self.YawSlide:SetPos(5, 285 + 15 + 5) 118 | self.YawSlide:SetSize(300, 30) 119 | 120 | self.RSlide:SetPos(5, 285 + 30 + 10) 121 | self.RSlide:SetSize(300, 30) 122 | 123 | self.OffsetCheck:SetPos(5, self:GetTall() - 28 - 15) 124 | self.OffsetCheck:SizeToContents() 125 | 126 | self.Spawn:SetPos(self:GetWide() - 60 - 5, self:GetTall() - 28) 127 | self.Spawn:SetSize(60, 20) 128 | 129 | end 130 | 131 | function PANEL:SetEntities(entities) 132 | self.Origins:UpdateLines(entities) 133 | self.EntityList:UpdateLines(entities) 134 | end 135 | 136 | function PANEL:SpawnSelected() 137 | local _, selectedEntity = self.EntityList:GetSelectedLine() 138 | if not SaveFile or not selectedEntity then return end 139 | self:OnSpawnRequested(SaveFile, selectedEntity:GetValue(1), false) 140 | end 141 | 142 | function PANEL:SetSaveFile(path) 143 | SaveFile = path 144 | if not SaveFile then 145 | self.Origins:Clear() 146 | self.EntityList:Clear() 147 | end 148 | end 149 | 150 | function PANEL:OnOriginRequested(path, modelname, loadFromClient) end 151 | function PANEL:OnModelRequested(path, modelname, loadFromClient) end 152 | function PANEL:OnSpawnRequested(path, modelName, loadFromClient) end 153 | function PANEL:SetOffsetMode(set) end 154 | 155 | vgui.Register("SMHSpawn", PANEL, "DFrame") 156 | -------------------------------------------------------------------------------- /lua/smh/client/derma/world_clicker.lua: -------------------------------------------------------------------------------- 1 | local BaseClass = baseclass.Get("EditablePanel") 2 | local PANEL = {} 3 | 4 | function PANEL:Init() 5 | 6 | self:SetWorldClicker(true) 7 | self.m_bStretchToFit = true 8 | 9 | self:SetPos(0, 0) 10 | self:SetSize(ScrW(), ScrH()) 11 | 12 | self:MakePopup() 13 | self:SetVisible(false) 14 | 15 | end 16 | 17 | function PANEL:SetVisible(visible) 18 | if not visible then 19 | RememberCursorPosition() 20 | end 21 | BaseClass.SetVisible(self, visible) 22 | if visible then 23 | RestoreCursorPosition() 24 | end 25 | end 26 | 27 | function PANEL:OnMousePressed(mousecode) 28 | if mousecode ~= MOUSE_RIGHT then 29 | return 30 | end 31 | 32 | local trace = util.TraceLine(util.GetPlayerTrace(LocalPlayer())) 33 | if not IsValid(trace.Entity) then return end 34 | 35 | local setting = 0 36 | if input.IsKeyDown(KEY_LSHIFT) then setting = 1 end 37 | 38 | self:OnEntitySelected(trace.Entity, setting) 39 | end 40 | 41 | function PANEL:OnEntitySelected(entity, setting) end 42 | 43 | vgui.Register("SMHWorldClicker", PANEL, "EditablePanel") 44 | -------------------------------------------------------------------------------- /lua/smh/client/highlighter.lua: -------------------------------------------------------------------------------- 1 | -- Most taken from lua/includes/modules/halo.lua 2 | -- https://github.com/Facepunch/garrysmod/blob/e47ac049d026f922867ee3adb2c4746fb1244300/garrysmod/lua/includes/modules/halo.lua#L38 3 | -- local matColor = Material( "model_color" ) 4 | local mat_Copy = Material( "pp/copy" ) 5 | local mat_Add = Material( "pp/add" ) 6 | -- local mat_Sub = Material( "pp/sub" ) 7 | local rt_Stencil = render.GetBloomTex0() 8 | local rt_Store = render.GetScreenEffectTexture( 0 ) 9 | local function RenderHalo(entities) 10 | 11 | local OldRT = render.GetRenderTarget() 12 | 13 | -- Copy what's currently on the screen to another texture 14 | render.CopyRenderTargetToTexture( rt_Store ) 15 | 16 | -- Clear the colour and the stencils, not the depth 17 | render.Clear( 0, 0, 0, 255, false, true ) 18 | 19 | 20 | -- FILL STENCIL 21 | -- Write to the stencil.. 22 | cam.Start3D( EyePos(), EyeAngles() ) 23 | 24 | render.SetStencilEnable( true ) 25 | render.SuppressEngineLighting(true) 26 | cam.IgnoreZ( false ) 27 | 28 | render.SetStencilWriteMask( 1 ) 29 | render.SetStencilTestMask( 1 ) 30 | render.SetStencilReferenceValue( 1 ) 31 | 32 | render.SetStencilCompareFunction( STENCILCOMPARISONFUNCTION_ALWAYS ) 33 | render.SetStencilPassOperation( STENCILOPERATION_REPLACE ) 34 | render.SetStencilFailOperation( STENCILOPERATION_KEEP ) 35 | render.SetStencilZFailOperation( STENCILOPERATION_KEEP ) 36 | 37 | for entity, _ in pairs(entities) do 38 | if IsValid(entity) then 39 | entity:DrawModel() 40 | end 41 | end 42 | 43 | render.SetStencilCompareFunction( STENCILCOMPARISONFUNCTION_EQUAL ) 44 | render.SetStencilPassOperation( STENCILOPERATION_KEEP ) 45 | 46 | cam.Start2D() 47 | surface.SetDrawColor(0, 255, 0) 48 | surface.DrawRect(0, 0, ScrW(), ScrH()) 49 | cam.End2D() 50 | 51 | render.SuppressEngineLighting(false) 52 | render.SetStencilEnable(false) 53 | cam.End3D() 54 | 55 | -- BLUR IT 56 | render.CopyRenderTargetToTexture( rt_Stencil ) 57 | render.BlurRenderTarget( rt_Stencil, 2, 2, 1 ) 58 | 59 | -- Put our scene back 60 | render.SetRenderTarget( OldRT ) 61 | mat_Copy:SetTexture( "$basetexture", rt_Store ) 62 | mat_Copy:SetString( "$color", "1 1 1" ) 63 | mat_Copy:SetString( "$alpha", "1" ) 64 | render.SetMaterial( mat_Copy ) 65 | render.DrawScreenQuad() 66 | 67 | -- DRAW IT TO THE SCEEN 68 | render.SetStencilEnable( true ) 69 | 70 | render.SetStencilCompareFunction( STENCILCOMPARISONFUNCTION_NOTEQUAL ) 71 | 72 | mat_Add:SetTexture( "$basetexture", rt_Stencil ) 73 | render.SetMaterial( mat_Add ) 74 | 75 | for i=0, 2 do 76 | render.DrawScreenQuad() 77 | end 78 | 79 | render.SetStencilEnable( false ) 80 | 81 | -- PUT EVERYTHING BACK HOW WE FOUND IT 82 | 83 | render.SetStencilWriteMask( 0 ) 84 | render.SetStencilReferenceValue( 0 ) 85 | render.SetStencilTestMask( 0 ) 86 | 87 | end 88 | 89 | hook.Add("PostDrawEffects", "smh_highlighter", function() 90 | if not next(SMH.State.Entity) or not SMH.Controller.ShouldHighlight() then 91 | return 92 | end 93 | 94 | RenderHalo(SMH.State.Entity) 95 | end) 96 | -------------------------------------------------------------------------------- /lua/smh/client/physrecord.lua: -------------------------------------------------------------------------------- 1 | local SMHRecorderID = "SMH_Recording_Timer" 2 | local Active = false 3 | local Waiting = 0 4 | 5 | surface.CreateFont( "smh_font", { 6 | font = "Arial", 7 | extended = false, 8 | size = 90, 9 | weight = 500, 10 | blursize = 0, 11 | scanlines = 4, 12 | antialias = true, 13 | underline = false, 14 | italic = false, 15 | strikeout = false, 16 | symbol = false, 17 | rotary = false, 18 | shadow = false, 19 | additive = false, 20 | outline = false 21 | } ) 22 | 23 | local MGR = {} 24 | 25 | MGR.FrameCount, MGR.RecordInterval, MGR.StartDelay = 100, 0, 3 26 | MGR.SelectedEntities = {} 27 | 28 | function MGR.RecordToggle() 29 | 30 | if not Active then 31 | SMH.Controller.SelectEntity(nil, {}) 32 | Active = true 33 | local wait = MGR.StartDelay 34 | Waiting = wait 35 | 36 | timer.Create(SMHRecorderID, 1 , wait + 1, function() 37 | Waiting = Waiting - 1 38 | end) 39 | 40 | timer.Create(SMHRecorderID .. 1, wait, 1, function() 41 | Waiting = 0 42 | SMH.Controller.StartPhysicsRecord(MGR.FrameCount, MGR.RecordInterval, MGR.SelectedEntities) 43 | timer.Remove(SMHRecorderID) 44 | end) 45 | else 46 | Active = false 47 | Waiting = 0 48 | SMH.Controller.StopPhysicsRecord() 49 | timer.Remove(SMHRecorderID) 50 | timer.Remove(SMHRecorderID .. 1) 51 | MGR.SelectedEntities = {} 52 | end 53 | 54 | end 55 | 56 | function MGR.Stop() 57 | Active = false 58 | Waiting = 0 59 | timer.Remove(SMHRecorderID) 60 | timer.Remove(SMHRecorderID .. 1) 61 | MGR.SelectedEntities = {} 62 | end 63 | 64 | function MGR.IsActive() 65 | return Active 66 | end 67 | 68 | SMH.PhysRecord = MGR 69 | 70 | hook.Add( "HUDPaint", "smh_draw_waiting", function() 71 | if Waiting > 0 then 72 | surface.SetFont( "smh_font" ) 73 | surface.SetTextColor( 255, 0, 0 ) 74 | surface.SetTextPos( 128, 128 ) 75 | surface.DrawText( "Starting physics recording in: " .. Waiting ) 76 | end 77 | end) 78 | -------------------------------------------------------------------------------- /lua/smh/client/renderer.lua: -------------------------------------------------------------------------------- 1 | local RenderCmd = "" 2 | local IsRendering = false 3 | 4 | local MGR = {} 5 | 6 | function MGR.IsRendering() 7 | return IsRendering 8 | end 9 | 10 | function MGR.Stop() 11 | LocalPlayer():EmitSound("buttons/button1.wav") 12 | 13 | IsRendering = false 14 | SMH.Controller.SetRendering(IsRendering) 15 | end 16 | 17 | local function RenderTick() 18 | if not IsRendering then 19 | return 20 | end 21 | 22 | local newPos = SMH.State.Frame + 1 23 | 24 | LocalPlayer():ConCommand(RenderCmd) 25 | 26 | if newPos >= SMH.State.PlaybackLength then 27 | MGR.Stop() 28 | return 29 | end 30 | 31 | timer.Simple(0.001, function() 32 | SMH.Controller.SetFrame(newPos) 33 | 34 | timer.Simple(0.001, function() 35 | RenderTick() 36 | end) 37 | end) 38 | 39 | end 40 | 41 | function MGR.Start(renderCmd, StartFrame) 42 | if not isstring(renderCmd) then error(string.format("Tried to start a render with a non-string renderCmd: %s: %q", type(renderCmd), tostring(renderCmd))) end 43 | RenderCmd = renderCmd 44 | 45 | IsRendering = true 46 | SMH.Controller.SetRendering(IsRendering) 47 | 48 | SMH.Controller.SetFrame(StartFrame) 49 | 50 | LocalPlayer():EmitSound("buttons/blip1.wav") 51 | 52 | timer.Simple(1, RenderTick) 53 | end 54 | 55 | SMH.Renderer = MGR 56 | -------------------------------------------------------------------------------- /lua/smh/client/settings.lua: -------------------------------------------------------------------------------- 1 | local ConVarType = { 2 | Bool = 1, 3 | Int = 2, 4 | Float = 3, 5 | } 6 | 7 | local GhostVars = { 8 | smh_ghostprevframe = "GhostPrevFrame", 9 | smh_ghostnextframe = "GhostNextFrame", 10 | smh_ghostallentities = "GhostAllEntities", 11 | smh_ghosttransparency = "GhostTransparency", 12 | smh_onionskin = "OnionSkin" 13 | } 14 | 15 | local TYPED_CV = {} 16 | TYPED_CV.__index = TYPED_CV 17 | 18 | function TYPED_CV:GetValue() 19 | if self.Type == ConVarType.Bool then 20 | return self.ConVar:GetBool() 21 | elseif self.Type == ConVarType.Int then 22 | return self.ConVar:GetInt() 23 | elseif self.Type == ConVarType.Float then 24 | return self.ConVar:GetFloat() 25 | end 26 | end 27 | 28 | function TYPED_CV:SetValue(value) 29 | if self.Type == ConVarType.Bool then 30 | return self.ConVar:SetBool(value) 31 | elseif self.Type == ConVarType.Int then 32 | return self.ConVar:SetInt(value) 33 | elseif self.Type == ConVarType.Float then 34 | return self.ConVar:SetFloat(value) 35 | end 36 | end 37 | 38 | local function CreateTypedConVar(type, name, defaultValue, helptext) 39 | if type == ConVarType.Bool then 40 | defaultValue = tostring(defaultValue and 1 or 0) 41 | elseif type == ConVarType.Int then 42 | defaultValue = tostring(defaultValue) 43 | elseif type == ConVarType.Float then 44 | defaultValue = tostring(defaultValue) 45 | end 46 | 47 | local cv = { 48 | Type = type, 49 | ConVar = CreateClientConVar(name, defaultValue, true, false, helptext, nil, nil), 50 | } 51 | setmetatable(cv, TYPED_CV) 52 | 53 | if GhostVars[name] then 54 | cvars.AddChangeCallback(name, function(convar, oldvalue, newvalue) 55 | if oldvalue == newvalue then return end 56 | 57 | SMH.Controller.UpdateUISetting(GhostVars[name], newvalue) 58 | SMH.Controller.UpdateGhostState() 59 | end) 60 | end 61 | 62 | return cv 63 | end 64 | 65 | local ConVars = { 66 | FreezeAll = CreateTypedConVar(ConVarType.Bool, "smh_freezeall", true), 67 | LocalizePhysBones = CreateTypedConVar(ConVarType.Bool, "smh_localizephysbones", false), 68 | IgnorePhysBones = CreateTypedConVar(ConVarType.Bool, "smh_ignorephysbones", false), 69 | GhostPrevFrame = CreateTypedConVar(ConVarType.Bool, "smh_ghostprevframe", false), 70 | GhostNextFrame = CreateTypedConVar(ConVarType.Bool, "smh_ghostnextframe", false), 71 | GhostAllEntities = CreateTypedConVar(ConVarType.Bool, "smh_ghostallentities", false), 72 | GhostTransparency = CreateTypedConVar(ConVarType.Float, "smh_ghosttransparency", 0.5), 73 | OnionSkin = CreateTypedConVar(ConVarType.Bool, "smh_onionskin", false), 74 | TweenDisable = CreateTypedConVar(ConVarType.Bool, "smh_tweendisable", false), 75 | SmoothPlayback = CreateTypedConVar(ConVarType.Bool, "smh_smoothplayback", false), 76 | EnableWorld = CreateTypedConVar(ConVarType.Bool, "smh_enableworldkeyframes", false), 77 | } 78 | 79 | 80 | local MGR = {} 81 | 82 | function MGR.GetAll() 83 | local settings = {} 84 | 85 | for name, convar in pairs(ConVars) do 86 | settings[name] = convar:GetValue() 87 | end 88 | 89 | return settings 90 | end 91 | 92 | function MGR.Update(newSettings) 93 | for name, value in pairs(newSettings) do 94 | if not ConVars[name] then 95 | continue 96 | end 97 | ConVars[name]:SetValue(value) 98 | end 99 | end 100 | 101 | SMH.Settings = MGR 102 | -------------------------------------------------------------------------------- /lua/smh/client/state.lua: -------------------------------------------------------------------------------- 1 | SMH.State = { 2 | Entity = {}, 3 | Frame = 0, 4 | Timeline = 1, 5 | 6 | PlaybackRate = 30, 7 | PlaybackLength = 100, 8 | } 9 | -------------------------------------------------------------------------------- /lua/smh/modifiers/advcamera.lua: -------------------------------------------------------------------------------- 1 | 2 | MOD.Name = "Advanced Cameras"; 3 | 4 | function MOD:IsAdvCamera(entity) 5 | 6 | if entity:GetClass() ~= "hl_camera" then return false; end 7 | return true; 8 | 9 | end 10 | 11 | function MOD:Save(entity) 12 | 13 | if not self:IsAdvCamera(entity) then return nil; end 14 | 15 | local data = {}; 16 | 17 | data.FOV = entity:GetFOV(); 18 | data.Nearz = entity:GetNearZ(); 19 | data.Farz = entity:GetFarZ(); 20 | data.Roll = entity:GetRoll(); 21 | data.Offset = entity:GetViewOffset(); 22 | 23 | return data; 24 | 25 | end 26 | 27 | function MOD:Load(entity, data) 28 | 29 | if not self:IsAdvCamera(entity) then return; end -- can never be too sure? 30 | 31 | entity:SetFOV(data.FOV); 32 | entity:SetNearZ(data.Nearz); 33 | entity:SetFarZ(data.Farz); 34 | entity:SetRoll(data.Roll); 35 | entity:SetViewOffset(data.Offset); 36 | 37 | end 38 | 39 | function MOD:LoadBetween(entity, data1, data2, percentage) 40 | 41 | if not self:IsAdvCamera(entity) then return; end -- can never be too sure? 42 | 43 | entity:SetFOV(SMH.LerpLinear(data1.FOV, data2.FOV, percentage)); 44 | entity:SetNearZ(SMH.LerpLinear(data1.Nearz, data2.Nearz, percentage)); 45 | entity:SetFarZ(SMH.LerpLinear(data1.Farz, data2.Farz, percentage)); 46 | entity:SetRoll(SMH.LerpLinear(data1.Roll, data2.Roll, percentage)); 47 | entity:SetViewOffset(SMH.LerpLinearVector(data1.Offset, data2.Offset, percentage)); 48 | 49 | end 50 | -------------------------------------------------------------------------------- /lua/smh/modifiers/advlights.lua: -------------------------------------------------------------------------------- 1 | 2 | MOD.Name = "Advanced Lights"; 3 | 4 | local validClasses = { 5 | projected_light = true, 6 | projected_light_new = true, 7 | cheap_light = true, 8 | expensive_light = true, 9 | expensive_light_new = true, 10 | spot_light = true 11 | }; 12 | 13 | function MOD:IsAdvLight(entity) 14 | 15 | local theclass = entity:GetClass(); 16 | 17 | return validClasses[theclass] or false; 18 | 19 | end 20 | 21 | function MOD:IsProjectedLight(entity) 22 | 23 | local theclass = entity:GetClass(); 24 | 25 | if theclass == "cheap_light" or theclass == "spot_light" then return false; end 26 | return true; 27 | 28 | end 29 | 30 | function MOD:Save(entity) 31 | 32 | if not self:IsAdvLight(entity) then return nil; end 33 | 34 | local data = {}; 35 | 36 | data.Brightness = entity:GetBrightness(); 37 | data.Color = entity:GetLightColor(); 38 | 39 | if self:IsProjectedLight(entity) then 40 | local theclass = entity:GetClass(); 41 | if theclass ~= "expensive_light" and theclass ~= "expensive_light_new" then -- expensive lights don't have FoV settings, but they are projected lights 42 | data.FOV = entity:GetLightFOV(); 43 | end 44 | if theclass == "projected_light_new" then 45 | data.OrthoBottom = entity:GetOrthoBottom(); 46 | data.OrthoLeft = entity:GetOrthoLeft(); 47 | data.OrthoRight = entity:GetOrthoRight(); 48 | data.OrthoTop = entity:GetOrthoTop(); 49 | end 50 | data.Nearz = entity:GetNearZ(); 51 | data.Farz = entity:GetFarZ(); 52 | elseif entity:GetClass() == "cheap_light" then 53 | data.LightSize = entity:GetLightSize(); 54 | else 55 | data.InFOV = entity:GetInnerFOV(); 56 | data.OutFOV = entity:GetOuterFOV(); 57 | data.Radius = entity:GetRadius(); 58 | end 59 | 60 | return data; 61 | 62 | end 63 | 64 | function MOD:Load(entity, data) 65 | 66 | if not self:IsAdvLight(entity) then return; end -- can never be too sure? 67 | 68 | entity:SetBrightness(data.Brightness); 69 | entity:SetLightColor(data.Color); 70 | 71 | if self:IsProjectedLight(entity) then 72 | local theclass = entity:GetClass(); 73 | if theclass ~= "expensive_light" and theclass ~= "expensive_light_new" then 74 | entity:SetLightFOV(data.FOV); 75 | end 76 | if theclass == "projected_light_new" then 77 | entity:SetOrthoBottom(data.OrthoBottom); 78 | entity:SetOrthoLeft(data.OrthoLeft); 79 | entity:SetOrthoRight(data.OrthoRight); 80 | entity:SetOrthoTop(data.OrthoTop); 81 | end 82 | entity:SetNearZ(data.Nearz); 83 | entity:SetFarZ(data.Farz); 84 | elseif entity:GetClass() == "cheap_light" then 85 | entity:SetLightSize(data.LightSize); 86 | else 87 | entity:SetInnerFOV(data.InFOV); 88 | entity:SetOuterFOV(data.OutFOV); 89 | entity:SetRadius(data.Radius); 90 | end 91 | 92 | end 93 | 94 | function MOD:LoadBetween(entity, data1, data2, percentage) 95 | 96 | if not self:IsAdvLight(entity) then return; end -- can never be too sure? 97 | 98 | entity:SetBrightness(SMH.LerpLinear(data1.Brightness, data2.Brightness, percentage)); 99 | entity:SetLightColor(SMH.LerpLinearVector(data1.Color, data2.Color, percentage)); 100 | 101 | if self:IsProjectedLight(entity) then 102 | local theclass = entity:GetClass(); 103 | if theclass ~= "expensive_light" and theclass ~= "expensive_light_new" then 104 | entity:SetLightFOV(SMH.LerpLinear(data1.FOV, data2.FOV, percentage)); 105 | end 106 | if theclass == "projected_light_new" then 107 | entity:SetOrthoBottom(SMH.LerpLinear(data1.OrthoBottom, data2.OrthoBottom, percentage)); 108 | entity:SetOrthoLeft(SMH.LerpLinear(data1.OrthoLeft, data2.OrthoLeft, percentage)); 109 | entity:SetOrthoRight(SMH.LerpLinear(data1.OrthoRight, data2.OrthoRight, percentage)); 110 | entity:SetOrthoTop(SMH.LerpLinear(data1.OrthoTop, data2.OrthoTop, percentage)); 111 | end 112 | entity:SetNearZ(SMH.LerpLinear(data1.Nearz, data2.Nearz, percentage)); 113 | entity:SetFarZ(SMH.LerpLinear(data1.Farz, data2.Farz, percentage)); 114 | elseif entity:GetClass() == "cheap_light" then 115 | entity:SetLightSize(SMH.LerpLinear(data1.LightSize, data2.LightSize, percentage)); 116 | else 117 | entity:SetInnerFOV(SMH.LerpLinear(data1.InFOV, data2.InFOV, percentage)); 118 | entity:SetOuterFOV(SMH.LerpLinear(data1.OutFOV, data2.OutFOV, percentage)); 119 | entity:SetRadius(SMH.LerpLinear(data1.Radius, data2.Radius, percentage)); 120 | end 121 | 122 | end 123 | -------------------------------------------------------------------------------- /lua/smh/modifiers/bodygroup.lua: -------------------------------------------------------------------------------- 1 | 2 | MOD.Name = "Bodygroup"; 3 | 4 | function MOD:Save(entity) 5 | 6 | if self:IsEffect(entity) then 7 | entity = entity.AttachedEntity; 8 | end 9 | 10 | local data = {}; 11 | local bgs = entity:GetBodyGroups(); 12 | for _, bg in pairs(bgs) do 13 | data[bg.id] = entity:GetBodygroup(bg.id); 14 | end 15 | return data; 16 | end 17 | 18 | function MOD:LoadGhost(entity, ghost, data) 19 | self:Load(ghost, data); 20 | end 21 | 22 | function MOD:LoadGhostBetween(entity, ghost, data1, data2, percentage) 23 | self:LoadBetween(ghost, data1, data2, percentage); 24 | end 25 | 26 | function MOD:Load(entity, data) 27 | 28 | if self:IsEffect(entity) then 29 | entity = entity.AttachedEntity; 30 | end 31 | 32 | for id, value in pairs(data) do 33 | entity:SetBodygroup(id, value); 34 | end 35 | end 36 | 37 | function MOD:LoadBetween(entity, data1, data2, percentage) 38 | 39 | if self:IsEffect(entity) then 40 | entity = entity.AttachedEntity; 41 | end 42 | 43 | self:Load(entity, data1); 44 | 45 | end 46 | -------------------------------------------------------------------------------- /lua/smh/modifiers/bones.lua: -------------------------------------------------------------------------------- 1 | 2 | MOD.Name = "Nonphysical Bones"; 3 | 4 | function MOD:Save(entity) 5 | 6 | if self:IsEffect(entity) then 7 | entity = entity.AttachedEntity; 8 | end 9 | 10 | local count = entity:GetBoneCount(); 11 | 12 | local data = {}; 13 | 14 | for b = 0, count - 1 do 15 | 16 | local d = {}; 17 | d.Pos = entity:GetManipulateBonePosition(b); 18 | d.Ang = entity:GetManipulateBoneAngles(b); 19 | d.Scale = entity:GetManipulateBoneScale(b); 20 | 21 | data[b] = d; 22 | 23 | end 24 | 25 | return data; 26 | 27 | end 28 | 29 | function MOD:LoadGhost(entity, ghost, data) 30 | self:Load(ghost, data); 31 | end 32 | 33 | function MOD:LoadGhostBetween(entity, ghost, data1, data2, percentage) 34 | self:LoadBetween(ghost, data1, data2, percentage); 35 | end 36 | 37 | function MOD:Load(entity, data) 38 | 39 | if self:IsEffect(entity) then 40 | entity = entity.AttachedEntity; 41 | end 42 | 43 | local count = entity:GetBoneCount(); 44 | 45 | for b = 0, count - 1 do 46 | 47 | local d = data[b]; 48 | entity:ManipulateBonePosition(b, d.Pos); 49 | entity:ManipulateBoneAngles(b, d.Ang); 50 | entity:ManipulateBoneScale(b, d.Scale); 51 | 52 | end 53 | 54 | end 55 | 56 | function MOD:LoadBetween(entity, data1, data2, percentage) 57 | 58 | if self:IsEffect(entity) then 59 | entity = entity.AttachedEntity; 60 | end 61 | 62 | local count = entity:GetBoneCount(); 63 | 64 | for b = 0, count - 1 do 65 | 66 | local d1 = data1[b]; 67 | local d2 = data2[b]; 68 | 69 | local Pos = SMH.LerpLinearVector(d1.Pos, d2.Pos, percentage); 70 | local Ang = SMH.LerpLinearAngle(d1.Ang, d2.Ang, percentage); 71 | local Scale = SMH.LerpLinear(d1.Scale, d2.Scale, percentage); 72 | 73 | entity:ManipulateBonePosition(b, Pos); 74 | entity:ManipulateBoneAngles(b, Ang); 75 | entity:ManipulateBoneScale(b, Scale); 76 | 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /lua/smh/modifiers/color.lua: -------------------------------------------------------------------------------- 1 | 2 | MOD.Name = "Color"; 3 | 4 | function MOD:Save(entity) 5 | 6 | if self:IsEffect(entity) then 7 | entity = entity.AttachedEntity; 8 | end 9 | 10 | local color = entity:GetColor(); 11 | return { Color = color }; 12 | 13 | end 14 | 15 | function MOD:Load(entity, data) 16 | 17 | if self:IsEffect(entity) then 18 | entity = entity.AttachedEntity; 19 | end 20 | 21 | entity:SetColor(data.Color); 22 | 23 | end 24 | 25 | function MOD:LoadBetween(entity, data1, data2, percentage) 26 | 27 | if self:IsEffect(entity) then 28 | entity = entity.AttachedEntity; 29 | end 30 | 31 | local c1 = data1.Color; 32 | local c2 = data2.Color; 33 | 34 | local r = SMH.LerpLinear(c1.r, c2.r, percentage); 35 | local g = SMH.LerpLinear(c1.g, c2.g, percentage); 36 | local b = SMH.LerpLinear(c1.b, c2.b, percentage); 37 | local a = SMH.LerpLinear(c1.a, c2.a, percentage); 38 | 39 | entity:SetColor(Color(r, g, b, a)); 40 | 41 | end 42 | -------------------------------------------------------------------------------- /lua/smh/modifiers/eyetarget.lua: -------------------------------------------------------------------------------- 1 | 2 | MOD.Name = "Eye target"; 3 | 4 | function MOD:HasEyes(entity) 5 | 6 | local Eyes = entity:LookupAttachment("eyes"); 7 | 8 | if Eyes == 0 then return false; end 9 | return true; 10 | 11 | end 12 | 13 | function MOD:Save(entity) 14 | 15 | if self:IsEffect(entity) then 16 | entity = entity.AttachedEntity; 17 | end 18 | 19 | if not self:HasEyes(entity) then return nil; end 20 | 21 | local data = {}; 22 | 23 | data.EyeTarget = entity:GetEyeTarget(); 24 | 25 | return data; 26 | 27 | end 28 | 29 | function MOD:Load(entity, data) 30 | 31 | if self:IsEffect(entity) then 32 | entity = entity.AttachedEntity; 33 | end 34 | 35 | if not self:HasEyes(entity) then return; end --Shouldn't happen, but meh 36 | 37 | entity:SetEyeTarget(data.EyeTarget); 38 | 39 | end 40 | 41 | function MOD:LoadBetween(entity, data1, data2, percentage) 42 | 43 | if self:IsEffect(entity) then 44 | entity = entity.AttachedEntity; 45 | end 46 | 47 | if not self:HasEyes(entity) then return; end --Shouldn't happen, but meh 48 | 49 | local et = SMH.LerpLinearVector(data1.EyeTarget, data2.EyeTarget, percentage); 50 | 51 | entity:SetEyeTarget(et); 52 | 53 | end 54 | -------------------------------------------------------------------------------- /lua/smh/modifiers/flex.lua: -------------------------------------------------------------------------------- 1 | 2 | MOD.Name = "Facial flexes"; 3 | 4 | function MOD:Save(entity) 5 | 6 | if self:IsEffect(entity) then 7 | entity = entity.AttachedEntity; 8 | end 9 | 10 | local count = entity:GetFlexNum(); 11 | if count <= 0 then return nil; end 12 | 13 | local data = {}; 14 | 15 | data.Scale = entity:GetFlexScale(); 16 | 17 | data.Weights = {}; 18 | 19 | for i = 0, count - 1 do 20 | data.Weights[i] = entity:GetFlexWeight(i); 21 | end 22 | 23 | return data; 24 | 25 | end 26 | 27 | function MOD:LoadGhost(entity, ghost, data) 28 | self:Load(ghost, data); 29 | end 30 | 31 | function MOD:LoadGhostBetween(entity, ghost, data1, data2, percentage) 32 | self:LoadBetween(ghost, data1, data2, percentage); 33 | end 34 | 35 | function MOD:Load(entity, data) 36 | 37 | if self:IsEffect(entity) then 38 | entity = entity.AttachedEntity; 39 | end 40 | 41 | local count = entity:GetFlexNum(); 42 | if count <= 0 then return; end --Shouldn't happen, but meh 43 | 44 | entity:SetFlexScale(data.Scale); 45 | 46 | for i, f in pairs(data.Weights) do 47 | entity:SetFlexWeight(i, f); 48 | end 49 | 50 | end 51 | 52 | function MOD:LoadBetween(entity, data1, data2, percentage) 53 | 54 | if self:IsEffect(entity) then 55 | entity = entity.AttachedEntity; 56 | end 57 | 58 | local count = entity:GetFlexNum(); 59 | if count <= 0 then return; end --Shouldn't happen, but meh 60 | 61 | local scale = SMH.LerpLinear(data1.Scale, data2.Scale, percentage); 62 | entity:SetFlexScale(scale); 63 | 64 | for i = 0, count - 1 do 65 | 66 | local w1 = data1.Weights[i]; 67 | local w2 = data2.Weights[i]; 68 | local w = SMH.LerpLinear(w1, w2, percentage); 69 | 70 | entity:SetFlexWeight(i, w); 71 | 72 | end 73 | 74 | end 75 | -------------------------------------------------------------------------------- /lua/smh/modifiers/modelscale.lua: -------------------------------------------------------------------------------- 1 | MOD.Name = "Model scale"; 2 | 3 | function MOD:Save(entity) 4 | return { 5 | ModelScale = entity:GetModelScale(); 6 | }; 7 | end 8 | 9 | function MOD:LoadGhost(entity, ghost, data) 10 | self:Load(ghost, data); 11 | end 12 | 13 | function MOD:LoadGhostBetween(entity, ghost, data1, data2, percentage) 14 | self:LoadBetween(ghost, data1, data2, percentage); 15 | end 16 | 17 | function MOD:Load(entity, data) 18 | entity:SetModelScale(data.ModelScale); 19 | end 20 | 21 | function MOD:LoadBetween(entity, data1, data2, percentage) 22 | 23 | local lerpedModelScale = SMH.LerpLinear(data1.ModelScale, data2.ModelScale, percentage); 24 | entity:SetModelScale(lerpedModelScale); 25 | 26 | end 27 | -------------------------------------------------------------------------------- /lua/smh/modifiers/physbones.lua: -------------------------------------------------------------------------------- 1 | 2 | MOD.Name = "Physical Bones"; 3 | 4 | function MOD:Save(entity) 5 | 6 | local count = entity:GetPhysicsObjectCount(); 7 | if count <= 0 then return nil; end 8 | 9 | local data = {}; 10 | 11 | for i = 0, count - 1 do 12 | 13 | local pb = entity:GetPhysicsObjectNum(i); 14 | local parent = entity:GetPhysicsObjectNum(GetPhysBoneParent(entity, i)); 15 | 16 | local d = {}; 17 | 18 | d.Pos = pb:GetPos(); 19 | d.Ang = pb:GetAngles(); 20 | 21 | if parent then 22 | d.LocalPos, d.LocalAng = WorldToLocal(pb:GetPos(), pb:GetAngles(), parent:GetPos(), parent:GetAngles()); 23 | end 24 | 25 | d.Moveable = pb:IsMoveable(); 26 | 27 | data[i] = d; 28 | 29 | end 30 | 31 | return data; 32 | 33 | end 34 | 35 | function MOD:Load(entity, data, settings) 36 | 37 | if settings.IgnorePhysBones then 38 | return; 39 | end 40 | 41 | local count = entity:GetPhysicsObjectCount(); 42 | 43 | for i = 0, count - 1 do 44 | 45 | local pb = entity:GetPhysicsObjectNum(i); 46 | local parent = entity:GetPhysicsObjectNum(GetPhysBoneParent(entity, i)); 47 | 48 | local d = data[i]; 49 | 50 | if parent and settings.LocalizePhysBones and d.LocalPos and d.LocalAng then 51 | local pos, ang = LocalToWorld(d.LocalPos, d.LocalAng, parent:GetPos(), parent:GetAngles()); 52 | pb:SetPos(pos); 53 | pb:SetAngles(ang); 54 | else 55 | pb:SetPos(d.Pos); 56 | pb:SetAngles(d.Ang); 57 | end 58 | 59 | if settings.FreezeAll then 60 | pb:EnableMotion(false); 61 | else 62 | pb:EnableMotion(d.Moveable); 63 | end 64 | 65 | pb:Wake(); 66 | 67 | end 68 | 69 | end 70 | 71 | function MOD:LoadGhost(entity, ghost, data) 72 | 73 | local count = ghost:GetPhysicsObjectCount(); 74 | 75 | for i = 0, count - 1 do 76 | 77 | local pb = ghost:GetPhysicsObjectNum(i); 78 | 79 | pb:EnableMotion(true); 80 | pb:Wake(); 81 | 82 | local d = data[i]; 83 | pb:SetPos(d.Pos); 84 | pb:SetAngles(d.Ang); 85 | 86 | pb:EnableMotion(false); 87 | pb:Wake(); 88 | 89 | end 90 | 91 | end 92 | 93 | function MOD:LoadGhostBetween(entity, ghost, data1, data2, percentage) 94 | 95 | local count = ghost:GetPhysicsObjectCount(); 96 | 97 | for i = 0, count - 1 do 98 | 99 | local pb = ghost:GetPhysicsObjectNum(i); 100 | 101 | local d1 = data1[i]; 102 | local d2 = data2[i]; 103 | 104 | local Pos = SMH.LerpLinearVector(d1.Pos, d2.Pos, percentage); 105 | local Ang = SMH.LerpLinearAngle(d1.Ang, d2.Ang, percentage); 106 | 107 | pb:EnableMotion(false); 108 | pb:SetPos(Pos); 109 | pb:SetAngles(Ang); 110 | 111 | pb:Wake(); 112 | 113 | end 114 | end 115 | 116 | function MOD:LoadBetween(entity, data1, data2, percentage, settings) 117 | 118 | if settings.IgnorePhysBones then 119 | return; 120 | end 121 | 122 | local count = entity:GetPhysicsObjectCount(); 123 | 124 | for i = 0, count - 1 do 125 | local pb = entity:GetPhysicsObjectNum(i); 126 | 127 | local d1 = data1[i]; 128 | local d2 = data2[i]; 129 | 130 | local Pos = SMH.LerpLinearVector(d1.Pos, d2.Pos, percentage); 131 | local Ang = SMH.LerpLinearAngle(d1.Ang, d2.Ang, percentage); 132 | 133 | if settings.FreezeAll then 134 | pb:EnableMotion(false); 135 | else 136 | pb:EnableMotion(d1.Moveable); 137 | end 138 | pb:SetPos(Pos); 139 | pb:SetAngles(Ang); 140 | 141 | pb:Wake(); 142 | end 143 | 144 | end 145 | 146 | function MOD:Offset(data, origindata, worldvector, worldangle, hitpos) 147 | 148 | if not hitpos then 149 | hitpos = origindata[0].Pos; 150 | end 151 | 152 | local newdata = {}; 153 | 154 | for id, kdata in pairs(data) do 155 | 156 | local d = {}; 157 | local Pos, Ang = WorldToLocal(kdata.Pos, kdata.Ang, origindata[0].Pos, Angle(0, 0, 0)); 158 | d.Pos, d.Ang = LocalToWorld(Pos, Ang, worldvector, worldangle); 159 | d.Pos = d.Pos + hitpos; 160 | 161 | if kdata.LocalPos and kdata.LocalAng then -- those shouldn't change 162 | d.LocalPos, d.LocalAng = kdata.LocalPos, kdata.LocalAng; 163 | end 164 | 165 | d.Moveable = kdata.Moveable; 166 | 167 | newdata[id] = d; 168 | end 169 | 170 | return newdata; 171 | 172 | end 173 | 174 | function MOD:OffsetDupe(entity, data, origindata) 175 | 176 | local pb = entity:GetPhysicsObjectNum(0); 177 | if not IsValid(pb) then return nil end 178 | 179 | local entPos, entAng = pb:GetPos(), pb:GetAngles(); 180 | local newdata = {}; 181 | 182 | for id, kdata in pairs(data) do 183 | 184 | local d = {}; 185 | d.Pos, d.Ang = WorldToLocal(kdata.Pos, kdata.Ang, origindata[0].Pos, origindata[0].Ang); 186 | d.Pos, d.Ang = LocalToWorld(d.Pos, d.Ang, entPos, entAng); 187 | 188 | if kdata.LocalPos and kdata.LocalAng then -- those shouldn't change 189 | d.LocalPos, d.LocalAng = kdata.LocalPos, kdata.LocalAng; 190 | end 191 | 192 | d.Moveable = kdata.Moveable; 193 | 194 | newdata[id] = d; 195 | end 196 | 197 | return newdata; 198 | 199 | end 200 | -------------------------------------------------------------------------------- /lua/smh/modifiers/poseparameter.lua: -------------------------------------------------------------------------------- 1 | 2 | MOD.Name = "Pose parameters"; 3 | 4 | function MOD:Save(entity) 5 | 6 | local data = {}; 7 | 8 | local count = entity:GetNumPoseParameters(); 9 | for i = 0, count - 1 do 10 | local name = entity:GetPoseParameterName(i); 11 | data[name] = entity:GetPoseParameter(name); 12 | end 13 | 14 | return data; 15 | 16 | end 17 | 18 | function MOD:Load(entity, data) 19 | 20 | for name, value in pairs(data) do 21 | entity:SetPoseParameter(name, value); 22 | end 23 | 24 | end 25 | 26 | function MOD:LoadBetween(entity, data1, data2, percentage) 27 | 28 | for name, value1 in pairs(data1) do 29 | 30 | local value2 = data2[name]; 31 | if value1 and value2 then 32 | entity:SetPoseParameter(name, SMH.LerpLinear(value1, value2, percentage)); 33 | elseif value1 then 34 | entity:SetPoseParameter(name, value1); 35 | end 36 | 37 | end 38 | 39 | end 40 | -------------------------------------------------------------------------------- /lua/smh/modifiers/position.lua: -------------------------------------------------------------------------------- 1 | 2 | MOD.Name = "Position and Rotation"; 3 | 4 | function MOD:Save(entity) 5 | 6 | local data = {}; 7 | data.Pos = entity:GetPos(); 8 | data.Ang = entity:GetAngles(); 9 | return data; 10 | 11 | end 12 | 13 | function MOD:LoadGhost(entity, ghost, data) 14 | self:Load(ghost, data); 15 | end 16 | 17 | function MOD:LoadGhostBetween(entity, ghost, data1, data2, percentage) 18 | self:LoadBetween(ghost, data1, data2, percentage); 19 | end 20 | 21 | function MOD:Load(entity, data) 22 | 23 | entity:SetPos(data.Pos); 24 | entity:SetAngles(data.Ang); 25 | 26 | end 27 | 28 | function MOD:LoadBetween(entity, data1, data2, percentage) 29 | 30 | local Pos = SMH.LerpLinearVector(data1.Pos, data2.Pos, percentage); 31 | local Ang = SMH.LerpLinearAngle(data1.Ang, data2.Ang, percentage); 32 | 33 | entity:SetPos(Pos); 34 | entity:SetAngles(Ang); 35 | 36 | end 37 | 38 | function MOD:Offset(data, origindata, worldvector, worldangle, hitpos) 39 | 40 | if not hitpos then 41 | hitpos = origindata.Pos 42 | end 43 | 44 | local datanew = {}; 45 | local Pos, Ang = WorldToLocal(data.Pos, data.Ang, origindata.Pos, Angle(0, 0, 0)); 46 | datanew.Pos, datanew.Ang = LocalToWorld(Pos, Ang, worldvector, worldangle); 47 | datanew.Pos = datanew.Pos + hitpos; 48 | return datanew; 49 | 50 | end 51 | 52 | function MOD:OffsetDupe(entity, data, origindata) 53 | 54 | local entPos, entAng = entity:GetPos(), entity:GetAngles(); 55 | local datanew = {}; 56 | datanew.Pos, datanew.Ang = WorldToLocal(data.Pos, data.Ang, origindata.Pos, origindata.Ang); 57 | datanew.Pos, datanew.Ang = LocalToWorld(datanew.Pos, datanew.Ang, entPos, entAng); 58 | 59 | return datanew; 60 | 61 | end 62 | -------------------------------------------------------------------------------- /lua/smh/modifiers/skin.lua: -------------------------------------------------------------------------------- 1 | 2 | MOD.Name = "Skin"; 3 | 4 | function MOD:Save(entity) 5 | 6 | if self:IsEffect(entity) then 7 | entity = entity.AttachedEntity; 8 | end 9 | 10 | return entity:GetSkin(); 11 | end 12 | 13 | function MOD:LoadGhost(entity, ghost, data) 14 | self:Load(ghost, data); 15 | end 16 | 17 | function MOD:LoadGhostBetween(entity, ghost, data1, data2, percentage) 18 | self:LoadBetween(ghost, data1, data2, percentage); 19 | end 20 | 21 | function MOD:Load(entity, data) 22 | 23 | if self:IsEffect(entity) then 24 | entity = entity.AttachedEntity; 25 | end 26 | 27 | entity:SetSkin(data); 28 | end 29 | 30 | function MOD:LoadBetween(entity, data1, data2, percentage) 31 | 32 | if self:IsEffect(entity) then 33 | entity = entity.AttachedEntity; 34 | end 35 | 36 | self:Load(entity, data1); 37 | 38 | end 39 | -------------------------------------------------------------------------------- /lua/smh/modifiers/softlamps.lua: -------------------------------------------------------------------------------- 1 | 2 | MOD.Name = "Soft Lamps"; 3 | 4 | function MOD:IsSoftLamp(entity) 5 | 6 | if entity:GetClass() ~= "gmod_softlamp" then return false; end 7 | return true; 8 | 9 | end 10 | 11 | function MOD:Save(entity) 12 | 13 | if not self:IsSoftLamp(entity) then return nil; end 14 | 15 | local data = {}; 16 | 17 | data.FOV = entity:GetLightFOV(); 18 | data.Nearz = entity:GetNearZ(); 19 | data.Farz = entity:GetFarZ(); 20 | data.Brightness = entity:GetBrightness(); 21 | data.Color = entity:GetLightColor(); 22 | data.ShapeRadius = entity:GetShapeRadius(); 23 | data.FocalPoint = entity:GetFocalDistance(); 24 | data.Offset = entity:GetLightOffset(); 25 | 26 | return data; 27 | 28 | end 29 | 30 | function MOD:Load(entity, data) 31 | 32 | if not self:IsSoftLamp(entity) then return; end -- can never be too sure? 33 | 34 | entity:SetLightFOV(data.FOV); 35 | entity:SetNearZ(data.Nearz); 36 | entity:SetFarZ(data.Farz); 37 | entity:SetBrightness(data.Brightness); 38 | entity:SetLightColor(data.Color); 39 | entity:SetShapeRadius(data.ShapeRadius); 40 | entity:SetFocalDistance(data.FocalPoint); 41 | entity:SetLightOffset(data.Offset); 42 | 43 | end 44 | 45 | function MOD:LoadBetween(entity, data1, data2, percentage) 46 | 47 | if not self:IsSoftLamp(entity) then return; end -- can never be too sure? 48 | 49 | entity:SetLightFOV(SMH.LerpLinear(data1.FOV, data2.FOV, percentage)); 50 | entity:SetNearZ(SMH.LerpLinear(data1.Nearz, data2.Nearz, percentage)); 51 | entity:SetFarZ(SMH.LerpLinear(data1.Farz, data2.Farz, percentage)); 52 | entity:SetBrightness(SMH.LerpLinear(data1.Brightness, data2.Brightness, percentage)); 53 | entity:SetLightColor(SMH.LerpLinearVector(data1.Color, data2.Color, percentage)); 54 | entity:SetShapeRadius(SMH.LerpLinear(data1.ShapeRadius, data2.ShapeRadius, percentage)); 55 | entity:SetFocalDistance(SMH.LerpLinear(data1.FocalPoint, data2.FocalPoint, percentage)); 56 | entity:SetLightOffset(SMH.LerpLinearVector(data1.Offset, data2.Offset, percentage)); 57 | 58 | end 59 | -------------------------------------------------------------------------------- /lua/smh/server.lua: -------------------------------------------------------------------------------- 1 | include("shared.lua") 2 | 3 | include("server/controller.lua") 4 | include("server/easing.lua") 5 | include("server/eyetarget.lua") 6 | include("server/ghosts_manager.lua") 7 | include("server/keyframe_data.lua") 8 | include("server/keyframe_manager.lua") 9 | include("server/modifiers.lua") 10 | include("server/physrecord.lua") 11 | include("server/playback_manager.lua") 12 | include("server/properties_manager.lua") 13 | include("server/spawn_manager.lua") 14 | include("server/worldkeyframes_manager.lua") 15 | 16 | AddCSLuaFile("shared.lua") 17 | AddCSLuaFile("client.lua") 18 | 19 | local function FindRecursive(name, path, func) 20 | local files, dirs = file.Find(name .. "/*", path); 21 | for _, dir in pairs(dirs) do 22 | FindRecursive(name .. "/" .. dir, path, func); 23 | end 24 | for _, f in pairs(files) do 25 | func(name .. "/" .. f); 26 | end 27 | end 28 | 29 | local function AddCSPath(path) 30 | if string.sub(path, -4) == ".lua" then 31 | AddCSLuaFile(path); 32 | end 33 | end 34 | 35 | FindRecursive("smh/shared", "LUA", AddCSPath) 36 | FindRecursive("smh/client", "LUA", AddCSPath) 37 | 38 | Msg("SMH server initialized.\n") 39 | -------------------------------------------------------------------------------- /lua/smh/server/easing.lua: -------------------------------------------------------------------------------- 1 | --- 2 | -- Lerp methods 3 | --- 4 | 5 | function SMH.LerpLinear(s, e, p) 6 | 7 | return Lerp(p, s, e); 8 | 9 | end 10 | 11 | function SMH.LerpLinearVector(s, e, p) 12 | 13 | return LerpVector(p, s, e); 14 | 15 | end 16 | 17 | function SMH.LerpLinearAngle(s, e, p) 18 | 19 | return LerpAngle(p, s, e); 20 | 21 | end 22 | -------------------------------------------------------------------------------- /lua/smh/server/eyetarget.lua: -------------------------------------------------------------------------------- 1 | 2 | -- New eye target functions to save eye target vector 3 | 4 | local meta = FindMetaTable("Entity"); 5 | 6 | meta.SetEyeTargetOld = meta.SetEyeTarget; 7 | 8 | function meta:SetEyeTarget(vec) 9 | 10 | self:SetEyeTargetOld(vec); 11 | self.EyeVec = vec; 12 | 13 | end 14 | 15 | function meta:GetEyeTarget() 16 | 17 | if not self.EyeVec then 18 | self.EyeVec = Vector(180, 0, 0); 19 | end 20 | 21 | return self.EyeVec; 22 | 23 | end 24 | -------------------------------------------------------------------------------- /lua/smh/server/ghosts_manager.lua: -------------------------------------------------------------------------------- 1 | local GhostData = {} 2 | local LastFrame = 0 3 | local LastTimeline = 1 4 | local SpawnGhost, SpawnGhostData, GhostSettings = {}, {}, {} 5 | local SpawnOffsetOn, SpawnOriginData, OffsetPos, OffsetAng = {}, {}, {}, {} 6 | 7 | local function CreateGhost(player, entity, color, frame, ghostable) 8 | for _, ghost in ipairs(GhostData[player].Ghosts) do 9 | if ghost.Entity == entity and ghost.Frame == frame then return ghost end -- we already have a ghost on this entity for this frame, just return it. 10 | end 11 | 12 | local class = entity:GetClass() 13 | local model = entity:GetModel() 14 | 15 | local g 16 | if class == "prop_ragdoll" then 17 | g = ents.Create("prop_ragdoll") 18 | 19 | local flags = entity:GetSaveTable().spawnflags or 0 20 | if flags % (2 * 32768) >= 32768 then 21 | g:SetKeyValue("spawnflags",32768) 22 | g:SetSaveValue("m_ragdoll.allowStretch", true) 23 | end 24 | else 25 | g = ents.Create("prop_dynamic") 26 | 27 | if class == "prop_effect" and IsValid(entity.AttachedEntity) then 28 | model = entity.AttachedEntity:GetModel() 29 | end 30 | end 31 | 32 | g:SetModel(model) 33 | g:SetRenderMode(RENDERMODE_TRANSCOLOR) 34 | g:SetCollisionGroup(COLLISION_GROUP_NONE) 35 | g:SetNotSolid(true) 36 | g:SetColor(color) 37 | g:Spawn() 38 | 39 | g:SetPos(entity:GetPos()) 40 | g:SetAngles(entity:GetAngles()) 41 | 42 | g.SMHGhost = true 43 | g.Entity = entity 44 | g.Frame = frame 45 | g.Physbones = false 46 | 47 | table.insert(ghostable, g) 48 | 49 | return g 50 | end 51 | 52 | local function SetGhostFrame(entity, ghost, modifiers, modname) 53 | if modifiers[modname] ~= nil then 54 | SMH.Modifiers[modname]:LoadGhost(entity, ghost, modifiers[modname]) 55 | if modname == "physbones" then ghost.Physbones = true end 56 | end 57 | end 58 | 59 | local function SetGhostBetween(entity, ghost, data1, data2, modname, percentage) 60 | if data1[modname] ~= nil then 61 | SMH.Modifiers[modname]:LoadGhostBetween(entity, ghost, data1[modname], data2[modname], percentage) 62 | if modname == "physbones" then ghost.Physbones = true end 63 | end 64 | end 65 | 66 | local function ClearNoPhysGhosts(ghosts) 67 | for _, g in ipairs(ghosts) do 68 | if g:GetClass() == "prop_ragdoll" and not g.Physbones and IsValid(g) then 69 | g:Remove() 70 | end 71 | end 72 | end 73 | 74 | local MGR = {} 75 | 76 | MGR.IsRendering = false 77 | 78 | function MGR.SelectEntity(player, entities) 79 | if not GhostData[player] then 80 | GhostData[player] = { 81 | Entity = {}, 82 | Ghosts = {}, 83 | } 84 | end 85 | 86 | GhostData[player].Entity = table.Copy(entities) 87 | end 88 | 89 | function MGR.UpdateState(player, frame, settings, timeline, settimeline) 90 | LastFrame = frame 91 | LastTimeline = settimeline 92 | 93 | if not GhostData[player] then 94 | return 95 | end 96 | 97 | local ghosts = GhostData[player].Ghosts 98 | 99 | for _, ghost in pairs(ghosts) do 100 | if IsValid(ghost) then 101 | ghost:Remove() 102 | end 103 | end 104 | table.Empty(ghosts) 105 | 106 | if not settings.GhostPrevFrame and not settings.GhostNextFrame and not settings.OnionSkin or MGR.IsRendering then 107 | return 108 | end 109 | 110 | if not SMH.KeyframeData.Players[player] then 111 | return 112 | end 113 | 114 | local entities = SMH.KeyframeData.Players[player].Entities 115 | local _, gentity = next(GhostData[player].Entity) 116 | if not settings.GhostAllEntities and IsValid(gentity) and entities[gentity] then 117 | local oldentities = table.Copy(entities) 118 | entities = {} 119 | for _, entity in pairs(GhostData[player].Entity) do 120 | entities[entity] = oldentities[entity] 121 | end 122 | elseif not settings.GhostAllEntities then 123 | return 124 | end 125 | 126 | local alpha = settings.GhostTransparency * 255 127 | local selectedtime = settimeline 128 | if selectedtime > timeline.Timelines then -- this shouldn't really happen? 129 | selectedtime = 1 130 | end 131 | 132 | local filtermods = {} 133 | 134 | for _, name in ipairs(timeline.TimelineMods[selectedtime]) do 135 | filtermods[name] = true 136 | end 137 | 138 | for entity, keyframes in pairs(entities) do 139 | 140 | for name, _ in pairs(filtermods) do -- gonna apply used modifiers 141 | local prevKeyframe, nextKeyframe, lerpMultiplier = SMH.GetClosestKeyframes(keyframes, frame, true, name) 142 | if not prevKeyframe and not nextKeyframe then 143 | continue 144 | end 145 | 146 | if lerpMultiplier == 0 then 147 | if settings.GhostPrevFrame and prevKeyframe.Frame < frame then 148 | local g = CreateGhost(player, entity, Color(200, 0, 0, alpha), prevKeyframe.Frame, ghosts) 149 | SetGhostFrame(entity, g, prevKeyframe.Modifiers, name) 150 | elseif settings.GhostNextFrame and nextKeyframe.Frame > frame then 151 | local g = CreateGhost(player, entity, Color(0, 200, 0, alpha), nextKeyframe.Frame, ghosts) 152 | SetGhostFrame(entity, g, nextKeyframe.Modifiers, name) 153 | end 154 | else 155 | if settings.GhostPrevFrame then 156 | local g = CreateGhost(player, entity, Color(200, 0, 0, alpha), prevKeyframe.Frame, ghosts) 157 | SetGhostFrame(entity, g, prevKeyframe.Modifiers, name) 158 | end 159 | if settings.GhostNextFrame then 160 | local g = CreateGhost(player, entity, Color(0, 200, 0, alpha), nextKeyframe.Frame, ghosts) 161 | SetGhostFrame(entity, g, nextKeyframe.Modifiers, name) 162 | end 163 | end 164 | 165 | if settings.OnionSkin then 166 | for _, keyframe in pairs(keyframes) do 167 | if keyframe.Modifiers[name] then 168 | local g = CreateGhost(player, entity, Color(255, 255, 255, alpha), keyframe.Frame, ghosts) 169 | SetGhostFrame(entity, g, keyframe.Modifiers, name) 170 | end 171 | end 172 | end 173 | end 174 | 175 | for _, g in ipairs(ghosts) do 176 | 177 | if not (g.Entity == entity) then continue end 178 | 179 | for name, mod in pairs(SMH.Modifiers) do 180 | if filtermods[name] then continue end -- we used these modifiers already 181 | local IsSet = false 182 | for _, keyframe in pairs(keyframes) do 183 | if keyframe.Frame == g.Frame and keyframe.Modifiers[name] then 184 | SetGhostFrame(entity, g, keyframe.Modifiers, name) 185 | IsSet = true 186 | break 187 | end 188 | end 189 | 190 | if not IsSet then 191 | local prevKeyframe, nextKeyframe, lerpMultiplier = SMH.GetClosestKeyframes(keyframes, g.Frame, true, name) 192 | if not prevKeyframe then 193 | continue 194 | end 195 | 196 | if lerpMultiplier <= 0 or settings.TweenDisable then 197 | SetGhostFrame(entity, g, prevKeyframe.Modifiers, name) 198 | elseif lerpMultiplier >= 1 then 199 | SetGhostFrame(entity, g, nextKeyframe.Modifiers, name) 200 | else 201 | SetGhostBetween(entity, g, prevKeyframe.Modifiers, nextKeyframe.Modifiers, name, lerpMultiplier) 202 | end 203 | end 204 | end 205 | end 206 | 207 | ClearNoPhysGhosts(ghosts) -- need to delete ragdoll ghosts that don't have physbone modifier, or else they'll just keep falling through ground. 208 | end 209 | end 210 | 211 | function MGR.UpdateSettings(player, timeline, settings) 212 | MGR.UpdateState(player, LastFrame, settings, timeline, LastTimeline) 213 | end 214 | 215 | function MGR.SetSpawnPreview(class, modelpath, data, settings, player) 216 | if IsValid(SpawnGhost[player]) then 217 | SpawnGhost[player]:Remove() 218 | end 219 | SpawnGhost[player] = nil 220 | SpawnGhostData[player] = nil 221 | 222 | if class == "prop_ragdoll" and not data["physbones"] then 223 | player:ChatPrint("Stop Motion Helper: Can't set preview for the ragdoll as the save doesn't have Physical Bones modifier!") 224 | return 225 | end 226 | if not data["physbones"] and not data["position"] then 227 | player:ChatPrint("Stop Motion Helper: Can't set preview for the entity as the save doesn't have Physical Bones or Position and Rotation modifiers!") 228 | return 229 | end 230 | 231 | SpawnGhostData[player] = data 232 | GhostSettings[player] = settings 233 | 234 | if class == "prop_ragdoll" then 235 | SpawnGhost[player] = ents.Create("prop_ragdoll") 236 | else 237 | SpawnGhost[player] = ents.Create("prop_dynamic") 238 | end 239 | local alpha = settings.GhostTransparency * 255 240 | 241 | SpawnGhost[player]:SetModel(modelpath) 242 | SpawnGhost[player]:SetRenderMode(RENDERMODE_TRANSCOLOR) 243 | SpawnGhost[player]:SetCollisionGroup(COLLISION_GROUP_NONE) 244 | SpawnGhost[player]:SetNotSolid(true) 245 | SpawnGhost[player]:SetColor(Color(255, 255, 255, alpha)) 246 | SpawnGhost[player]:Spawn() 247 | 248 | for name, mod in pairs(SMH.Modifiers) do 249 | if name == "color" then continue end 250 | if name == "physbones" or name == "position" then 251 | local offsetpos = OffsetPos[player] or Vector(0, 0, 0) 252 | local offsetang = OffsetAng[player] or Angle(0, 0, 0) 253 | 254 | offsetdata = mod:Offset(data[name].Modifiers, SpawnOriginData[player][name].Modifiers, offsetpos, offsetang, nil) 255 | mod:Load(SpawnGhost[player], offsetdata, GhostSettings[player]) 256 | elseif data[name] then 257 | mod:Load(SpawnGhost[player], data[name].Modifiers, settings) 258 | end 259 | end 260 | end 261 | 262 | function MGR.RefreshSpawnPreview(player, offseton) 263 | SpawnOffsetOn[player] = offseton 264 | if not IsValid(SpawnGhost[player]) then return end 265 | 266 | for name, mod in pairs(SMH.Modifiers) do 267 | if name == "color" then continue end 268 | if name == "physbones" or name == "position" then 269 | local offsetpos = OffsetPos[player] or Vector(0, 0, 0) 270 | local offsetang = OffsetAng[player] or Angle(0, 0, 0) 271 | 272 | offsetdata = mod:Offset(SpawnGhostData[player][name].Modifiers, SpawnOriginData[player][name].Modifiers, offsetpos, offsetang, nil) 273 | mod:Load(SpawnGhost[player], offsetdata, GhostSettings[player]) 274 | elseif SpawnGhostData[player][name] then 275 | mod:Load(SpawnGhost[player], SpawnGhostData[player][name].Modifiers, GhostSettings[player]) 276 | end 277 | end 278 | end 279 | 280 | function MGR.SpawnClear(player) 281 | if IsValid(SpawnGhost[player]) then 282 | SpawnGhost[player]:Remove() 283 | SpawnGhost[player] = nil 284 | end 285 | end 286 | 287 | function MGR.SetSpawnOrigin(data, player) 288 | SpawnOriginData[player] = data 289 | end 290 | 291 | function MGR.ClearSpawnOrigin(player) 292 | SpawnOriginData[player] = nil 293 | end 294 | 295 | function MGR.SetPosOffset(pos, player) 296 | OffsetPos[player] = pos 297 | MGR.RefreshSpawnPreview(player, SpawnOffsetOn[player]) 298 | end 299 | 300 | function MGR.SetAngleOffset(ang, player) 301 | OffsetAng[player] = ang 302 | MGR.RefreshSpawnPreview(player, SpawnOffsetOn[player]) 303 | end 304 | 305 | SMH.GhostsManager = MGR 306 | 307 | hook.Add("Think", "SMHGhostSpawnOffsetPreview", function() 308 | for player, data in pairs(SpawnOriginData) do 309 | if SpawnOffsetOn[player] and IsValid(SpawnGhost[player]) then 310 | for name, mod in pairs(SMH.Modifiers) do 311 | if name == "color" then continue end 312 | if SpawnGhostData[player][name] and data[name] and (name == "physbones" or name == "position") then 313 | local offsetpos = OffsetPos[player] or Vector(0, 0, 0) 314 | local offsetang = OffsetAng[player] or Angle(0, 0, 0) 315 | 316 | offsetdata = mod:Offset(SpawnGhostData[player][name].Modifiers, data[name].Modifiers, offsetpos, offsetang, player:GetEyeTraceNoCursor().HitPos) 317 | mod:Load(SpawnGhost[player], offsetdata, GhostSettings[player]) 318 | end 319 | end 320 | end 321 | end 322 | end) 323 | -------------------------------------------------------------------------------- /lua/smh/server/keyframe_data.lua: -------------------------------------------------------------------------------- 1 | function SMH.GetClosestKeyframes(keyframes, frame, ignoreCurrentFrame, modname) 2 | if ignoreCurrentFrame == nil then 3 | ignoreCurrentFrame = false 4 | end 5 | 6 | local prevKeyframe = nil 7 | local nextKeyframe = nil 8 | for _, keyframe in pairs(keyframes) do 9 | if keyframe.Frame == frame and keyframe.Modifiers[modname] and not ignoreCurrentFrame then 10 | prevKeyframe = keyframe 11 | nextKeyframe = keyframe 12 | break 13 | end 14 | 15 | if keyframe.Frame < frame and (not prevKeyframe or prevKeyframe.Frame < keyframe.Frame) and keyframe.Modifiers[modname] then 16 | prevKeyframe = keyframe 17 | elseif keyframe.Frame > frame and (not nextKeyframe or nextKeyframe.Frame > keyframe.Frame) and keyframe.Modifiers[modname] then 18 | nextKeyframe = keyframe 19 | end 20 | end 21 | 22 | if not prevKeyframe and not nextKeyframe then 23 | return nil, nil, 0 24 | elseif not prevKeyframe then 25 | prevKeyframe = nextKeyframe 26 | elseif not nextKeyframe then 27 | nextKeyframe = prevKeyframe 28 | end 29 | 30 | local lerpMultiplier = 0 31 | if prevKeyframe.Frame ~= nextKeyframe.Frame then 32 | lerpMultiplier = (frame - prevKeyframe.Frame) / (nextKeyframe.Frame - prevKeyframe.Frame) 33 | lerpMultiplier = math.EaseInOut(lerpMultiplier, prevKeyframe.EaseOut[modname], nextKeyframe.EaseIn[modname]) 34 | end 35 | 36 | return prevKeyframe, nextKeyframe, lerpMultiplier 37 | end 38 | 39 | local META = {} 40 | META.__index = META 41 | 42 | function META:New(player, entity) 43 | local keyframe = { 44 | ID = self.NextKeyframeId, 45 | Entity = entity, 46 | Frame = -1, 47 | EaseIn = {}, 48 | EaseOut = {}, 49 | Modifiers = {} 50 | } 51 | self.NextKeyframeId = self.NextKeyframeId + 1 52 | 53 | if not self.Players[player] then 54 | self.Players[player] = { 55 | Keyframes = {}, 56 | Entities = {}, 57 | } 58 | end 59 | 60 | self.Players[player].Keyframes[keyframe.ID] = keyframe 61 | 62 | if not self.Players[player].Entities[entity] then 63 | self.Players[player].Entities[entity] = {} 64 | end 65 | 66 | table.insert(self.Players[player].Entities[entity], keyframe) 67 | 68 | return keyframe 69 | end 70 | 71 | function META:Delete(player, id) 72 | if not self.Players[player] or not self.Players[player].Keyframes[id] then 73 | return 74 | end 75 | 76 | local keyframe = self.Players[player].Keyframes[id] 77 | if self.Players[player].Entities[keyframe.Entity] then 78 | table.RemoveByValue(self.Players[player].Entities[keyframe.Entity], keyframe) 79 | end 80 | self.Players[player].Keyframes[id] = nil 81 | end 82 | 83 | SMH.KeyframeData = { 84 | NextKeyframeId = 0, 85 | Players = {}, 86 | } 87 | setmetatable(SMH.KeyframeData, META) 88 | -------------------------------------------------------------------------------- /lua/smh/server/keyframe_manager.lua: -------------------------------------------------------------------------------- 1 | local function GetExistingKeyframe(player, entity, frame, modnames) 2 | if not SMH.KeyframeData.Players[player] or not SMH.KeyframeData.Players[player].Entities[entity] then 3 | return nil 4 | end 5 | if not modnames then 6 | modnames = { "world" } 7 | for name, mod in pairs(SMH.Modifiers) do 8 | table.insert(modnames, name) 9 | end 10 | end 11 | 12 | local keyframes = SMH.KeyframeData.Players[player].Entities[entity] 13 | for _, keyframe in pairs(keyframes) do 14 | for _, name in ipairs(modnames) do 15 | if keyframe.Frame == frame and keyframe.Modifiers[name] then 16 | return keyframe 17 | end 18 | end 19 | end 20 | 21 | return nil 22 | end 23 | 24 | local function Record(keyframe, player, entity, modnames) 25 | local recorded = false 26 | for _, name in ipairs(modnames) do 27 | if not SMH.Modifiers[name] then continue end 28 | local data = SMH.Modifiers[name]:Save(entity) 29 | if not data then continue end 30 | recorded = true 31 | keyframe.Modifiers[name] = data 32 | keyframe.EaseIn[name] = keyframe.EaseIn[name] and keyframe.EaseIn[name] or 0 33 | keyframe.EaseOut[name] = keyframe.EaseOut[name] and keyframe.EaseOut[name] or 0 34 | end 35 | return recorded 36 | end 37 | 38 | local function ClearModifier(keyframe, modname) 39 | keyframe.Modifiers[modname] = nil 40 | keyframe.EaseIn[modname] = nil 41 | keyframe.EaseOut[modname] = nil 42 | end 43 | 44 | hook.Add("EntityRemoved", "SMHKeyframesEntityRemoved", function(entity) 45 | 46 | for _, player in pairs(player.GetAll()) do 47 | if SMH.KeyframeData.Players[player] and SMH.KeyframeData.Players[player].Entities[entity] then 48 | local keyframesToDelete = {} 49 | for _, keyframe in pairs(SMH.KeyframeData.Players[player].Entities[entity]) do 50 | table.insert(keyframesToDelete, keyframe.ID) 51 | end 52 | 53 | SMH.KeyframeData.Players[player].Entities[entity] = nil 54 | 55 | for _, keyframeId in pairs(keyframesToDelete) do 56 | SMH.KeyframeData:Delete(player, keyframeId) 57 | end 58 | end 59 | end 60 | 61 | end) 62 | 63 | local MGR = {} 64 | 65 | function MGR.GetAll(player) 66 | if not SMH.KeyframeData.Players[player] then 67 | return {} 68 | end 69 | local keyframes = SMH.KeyframeData.Players[player].Keyframes 70 | 71 | local result = {} 72 | for _, keyframe in pairs(keyframes) do 73 | table.insert(result, table.Copy(keyframe)) 74 | end 75 | return result 76 | end 77 | 78 | function MGR.GetAllForEntity(player, entities) 79 | local keyframes = {} 80 | 81 | for _, entity in ipairs(entities) do 82 | if not SMH.KeyframeData.Players[player] or not SMH.KeyframeData.Players[player].Entities[entity] then 83 | continue 84 | end 85 | 86 | for k, keyframe in pairs(SMH.KeyframeData.Players[player].Entities[entity]) do 87 | table.insert(keyframes, keyframe) 88 | end 89 | end 90 | 91 | return keyframes 92 | end 93 | 94 | function MGR.Create(player, entities, frame, timeline) 95 | local keyframes = {} 96 | 97 | for _, entity in ipairs(entities) do 98 | if player ~= entity then 99 | local modnames = SMH.Properties.Players[player].TimelineSetting.TimelineMods[timeline] 100 | local keyframe = GetExistingKeyframe(player, entity, frame) 101 | 102 | if keyframe ~= nil then 103 | local check = Record(keyframe, player, entity, modnames) 104 | if check then 105 | table.insert(keyframes, keyframe) 106 | end 107 | else 108 | keyframe = SMH.KeyframeData:New(player, entity) 109 | keyframe.Frame = frame 110 | local check = Record(keyframe, player, entity, modnames) 111 | if check then 112 | table.insert(keyframes, keyframe) 113 | end 114 | end 115 | else 116 | local keyframe = GetExistingKeyframe(player, entity, frame, {"world"}) 117 | 118 | if keyframe ~= nil then return {keyframe} end 119 | 120 | keyframe = SMH.KeyframeData:New(player, entity) 121 | keyframe.Frame = frame 122 | keyframe.EaseIn["world"] = 0 123 | keyframe.EaseOut["world"] = 0 124 | keyframe.Modifiers["world"] = { 125 | Console = "", 126 | Push = "", 127 | Release = "", 128 | } 129 | table.insert(keyframes, keyframe) 130 | end 131 | end 132 | 133 | return keyframes 134 | end 135 | 136 | function MGR.Update(player, keyframeIds, updateData, timeline) 137 | local keyframes, movingkeyframes = {}, {} 138 | 139 | for id, keyframeId in ipairs(keyframeIds) do 140 | if not SMH.KeyframeData.Players[player] or not SMH.KeyframeData.Players[player].Keyframes[keyframeId] then 141 | error("Invalid keyframe ID") 142 | end 143 | 144 | local keyframe = SMH.KeyframeData.Players[player].Keyframes[keyframeId] 145 | local modnames = player == keyframe.Entity and {"world"} or SMH.Properties.Players[player].TimelineSetting.TimelineMods[timeline] 146 | local updateableFields = { 147 | "Frame", 148 | "EaseIn", 149 | "EaseOut", 150 | } 151 | for _, field in pairs(updateableFields) do 152 | if updateData[id][field] then 153 | if field == "Frame" then 154 | if updateData[id][field] == keyframe.Frame then continue end 155 | local remainmods, EaseIn, EaseOut, frame = table.Copy(keyframe.Modifiers), table.Copy(keyframe.EaseIn), table.Copy(keyframe.EaseOut), updateData[id][field] 156 | for _, name in ipairs(modnames) do 157 | remainmods[name] = nil 158 | EaseIn[name] = nil 159 | EaseOut[name] = nil 160 | end 161 | 162 | if next(remainmods) then -- if there are any modifiers remaining, then we create another keyframe that will stay there 163 | local remainkeyframe = SMH.KeyframeData:New(player, keyframe.Entity) 164 | 165 | for name, _ in pairs(remainmods) do 166 | ClearModifier(keyframe, name) 167 | end 168 | remainkeyframe.Frame = keyframe.Frame 169 | remainkeyframe.Modifiers = remainmods 170 | remainkeyframe.EaseIn = EaseIn 171 | remainkeyframe.EaseOut = EaseOut 172 | end 173 | 174 | movingkeyframes[keyframe] = frame 175 | else 176 | for _, name in ipairs(modnames) do 177 | if not keyframe[field][name] then continue end 178 | keyframe[field][name] = updateData[id][field] 179 | end 180 | table.insert(keyframes, keyframe) 181 | end 182 | end 183 | end 184 | end 185 | 186 | for keyframe, frame in pairs(movingkeyframes) do 187 | local replacekey = GetExistingKeyframe(player, keyframe.Entity, frame) 188 | 189 | if replacekey ~= nil and not movingkeyframes[replacekey] then 190 | for name, data in pairs(replacekey.Modifiers) do 191 | 192 | if not keyframe.Modifiers[name] then 193 | keyframe.Modifiers[name] = data 194 | keyframe.EaseIn[name] = replacekey.EaseIn[name] 195 | keyframe.EaseOut[name] = replacekey.EaseOut[name] 196 | end 197 | end 198 | SMH.KeyframeData:Delete(player, replacekey.ID) 199 | end 200 | keyframe.Frame = frame 201 | table.insert(keyframes, keyframe) 202 | end 203 | 204 | return keyframes 205 | end 206 | 207 | function MGR.Copy(player, keyframeIds, frame, timeline) 208 | local copiedKeyframes, movingkeyframes = {}, {} 209 | 210 | for id, keyframeId in ipairs(keyframeIds) do 211 | if not SMH.KeyframeData.Players[player] or not SMH.KeyframeData.Players[player].Keyframes[keyframeId] then 212 | error("Invalid keyframe ID") 213 | end 214 | 215 | local keyframe = SMH.KeyframeData.Players[player].Keyframes[keyframeId] 216 | local modnames = player == keyframe.Entity and {"world"} or SMH.Properties.Players[player].TimelineSetting.TimelineMods[timeline] 217 | 218 | local EaseIn, EaseOut, Mods = {}, {}, {} 219 | for _, name in ipairs(modnames) do 220 | if not keyframe.Modifiers[name] then continue end 221 | EaseIn[name] = keyframe.EaseIn[name] 222 | EaseOut[name] = keyframe.EaseOut[name] 223 | if istable(keyframe.Modifiers[name]) then 224 | Mods[name] = table.Copy(keyframe.Modifiers[name]) -- in case if we copy things like world keyframes 225 | else 226 | Mods[name] = keyframe.Modifiers[name] 227 | end 228 | end 229 | 230 | local copiedKeyframe = SMH.KeyframeData:New(player, keyframe.Entity) 231 | copiedKeyframe.EaseIn = EaseIn 232 | copiedKeyframe.EaseOut = EaseOut 233 | copiedKeyframe.Modifiers = Mods 234 | 235 | movingkeyframes[copiedKeyframe] = frame[id] 236 | end 237 | 238 | for keyframe, frame in pairs(movingkeyframes) do 239 | local replacekey = GetExistingKeyframe(player, keyframe.Entity, frame) 240 | 241 | if replacekey ~= nil and not movingkeyframes[replacekey] then 242 | for name, data in pairs(replacekey.Modifiers) do 243 | if not keyframe.Modifiers[name] then 244 | keyframe.Modifiers[name] = data 245 | keyframe.EaseIn[name] = replacekey.EaseIn[name] 246 | keyframe.EaseOut[name] = replacekey.EaseOut[name] 247 | end 248 | end 249 | SMH.KeyframeData:Delete(player, replacekey.ID) 250 | end 251 | 252 | keyframe.Frame = frame 253 | table.insert(copiedKeyframes, keyframe) 254 | end 255 | 256 | return copiedKeyframes 257 | end 258 | 259 | function MGR.Delete(player, keyframeId, timeline) 260 | if not SMH.KeyframeData.Players[player] or not SMH.KeyframeData.Players[player].Keyframes[keyframeId] then 261 | error("Invalid keyframe ID") 262 | end 263 | 264 | local entity = SMH.KeyframeData.Players[player].Keyframes[keyframeId].Entity 265 | 266 | local keyframe = SMH.KeyframeData.Players[player].Keyframes[keyframeId] 267 | local modnames = player == keyframe.Entity and {"world"} or SMH.Properties.Players[player].TimelineSetting.TimelineMods[timeline] 268 | 269 | for _, name in ipairs(modnames) do 270 | ClearModifier(keyframe, name) 271 | end 272 | 273 | if not next(keyframe.Modifiers) then 274 | SMH.KeyframeData:Delete(player, keyframeId) 275 | end 276 | return entity 277 | end 278 | 279 | function MGR.ImportSave(player, entity, serializedKeyframes, entityProperties) 280 | if SMH.KeyframeData.Players[player] and SMH.KeyframeData.Players[player].Entities[entity] then 281 | local deletethis = table.Copy(SMH.KeyframeData.Players[player].Entities[entity]) 282 | for _, keyframe in pairs(deletethis) do 283 | SMH.KeyframeData:Delete(player, keyframe.ID) 284 | end 285 | end 286 | 287 | SMH.PropertiesManager.SetProperties(player, entity, entityProperties) 288 | 289 | for _, skf in pairs(serializedKeyframes) do 290 | local keyframe = GetExistingKeyframe(player, entity, skf.Position) -- check for SMH 3.0 save stuff as it has a system with multiple keyframes occupying same frame 291 | 292 | if keyframe ~= nil then 293 | for name, _ in pairs(skf.EntityData) do 294 | keyframe.EaseIn[name] = type(skf.EaseIn) == "table" and skf.EaseIn[name] or skf.EaseIn 295 | keyframe.EaseOut[name] = type(skf.EaseOut) == "table" and skf.EaseOut[name] or skf.EaseOut 296 | keyframe.Modifiers[name] = skf.EntityData[name] 297 | end 298 | else 299 | local keyframe = SMH.KeyframeData:New(player, entity) 300 | keyframe.Frame = skf.Position 301 | for name, _ in pairs(skf.EntityData) do 302 | keyframe.EaseIn[name] = type(skf.EaseIn) == "table" and skf.EaseIn[name] or skf.EaseIn 303 | keyframe.EaseOut[name] = type(skf.EaseOut) == "table" and skf.EaseOut[name] or skf.EaseOut 304 | keyframe.Modifiers[name] = skf.EntityData[name] 305 | end 306 | end 307 | end 308 | end 309 | 310 | function MGR.GetWorldData(player, frame) 311 | local keyframe = GetExistingKeyframe(player, player, frame, {"world"}) 312 | local modifiers = keyframe.Modifiers["world"] 313 | 314 | return modifiers.Console, modifiers.Push, modifiers.Release 315 | end 316 | 317 | function MGR.UpdateWorldKeyframe(player, frame, str, key) 318 | local keyframe = GetExistingKeyframe(player, player, frame, {"world"}) 319 | if not keyframe then return end 320 | keyframe.Modifiers["world"][key] = str 321 | end 322 | 323 | SMH.KeyframeManager = MGR 324 | -------------------------------------------------------------------------------- /lua/smh/server/modifiers.lua: -------------------------------------------------------------------------------- 1 | local MODBASE = {} 2 | MODBASE.__index = MODBASE 3 | MODBASE.Name = "Unnamed" 4 | 5 | function MODBASE:Save(entity) end 6 | function MODBASE:Load(entity, data, settings) end 7 | function MODBASE:LoadGhost(entity, ghost, data, settings) end 8 | function MODBASE:LoadBetween(entity, data1, data2, percentage, settings) end 9 | function MODBASE:LoadGhostBetween(entity, ghost, data1, data2, percentage, settings) end 10 | function MODBASE:Offset(data, origindata, worldvector, worldangle, offsetpos, offsetang) end 11 | function MODBASE:OffsetDupe(entity, data, origindata) end 12 | 13 | function MODBASE:IsEffect(entity) -- checking if the entity is an effect prop 14 | if entity:GetClass() == "prop_effect" and IsValid(entity.AttachedEntity) then return true end 15 | return false 16 | end 17 | 18 | SMH.Modifiers = {} 19 | 20 | local path = "smh/modifiers/" 21 | local files, dirs = file.Find(path .. "*.lua", "LUA") 22 | 23 | for _, f in pairs(files) do 24 | 25 | _G["MOD"] = setmetatable({}, MODBASE) 26 | 27 | include(path .. f) 28 | 29 | SMH.Modifiers[f:sub(1, -5)] = _G["MOD"] 30 | 31 | _G["MOD"] = nil 32 | 33 | end 34 | -------------------------------------------------------------------------------- /lua/smh/server/physrecord.lua: -------------------------------------------------------------------------------- 1 | local SMHRecorderID = "SMH_Recording_Timer" 2 | 3 | local MGR = {} 4 | 5 | local function RecordPhys(player, entities, timelines, frame) 6 | SMH.PropertiesManager.AddEntity(player, entities) 7 | 8 | for _, entity in ipairs(entities) do 9 | local totaltimelines = SMH.PropertiesManager.GetTimelines(player) 10 | if timelines[entity] > totaltimelines then timelines[entity] = 1 end 11 | 12 | SMH.KeyframeManager.Create(player, {entity}, frame, timelines[entity]) 13 | end 14 | end 15 | 16 | function MGR.RecordStart(player, framecount, interval, frame, playbackrate, endframe, entities, timelines, settings) 17 | if framecount < 3 then framecount = 3 end 18 | if interval < 0 then interval = 0 end 19 | local counter = -1 20 | RecordPhys(player, entities, timelines, frame) 21 | 22 | timer.Create(SMHRecorderID .. player:EntIndex(), 1 / playbackrate , framecount, function() 23 | counter = counter + 1 24 | 25 | if interval == 0 or (counter / interval) == math.Round(counter / interval) then 26 | RecordPhys(player, entities, timelines, frame) 27 | end 28 | 29 | if counter >= framecount - 1 or frame + 1 > endframe - 1 then 30 | RecordPhys(player, entities, timelines, frame) 31 | timer.Remove(SMHRecorderID .. player:EntIndex()) 32 | player:ChatPrint( "SMH Physics Recorder stopped.") 33 | SMH.Controller.StopPhysicsRecordResponse(player) 34 | else 35 | frame = frame + 1 36 | SMH.PlaybackManager.SetFrameIgnore(player, frame, settings, timelines) 37 | end 38 | end) 39 | 40 | end 41 | 42 | function MGR.RecordStop(player) 43 | timer.Remove(SMHRecorderID .. player:EntIndex()) 44 | player:ChatPrint( "SMH Physics Recorder stopped.") 45 | end 46 | 47 | SMH.PhysRecord = MGR 48 | -------------------------------------------------------------------------------- /lua/smh/server/playback_manager.lua: -------------------------------------------------------------------------------- 1 | local ActivePlaybacks = {} 2 | 3 | local MGR = {} 4 | 5 | local function PlaybackSmooth(player, playback, settings) 6 | if not SMH.KeyframeData.Players[player] then 7 | return 8 | end 9 | 10 | playback.Timer = playback.Timer + FrameTime() 11 | local timePerFrame = 1 / playback.PlaybackRate 12 | 13 | playback.CurrentFrame = playback.Timer / timePerFrame + playback.StartFrame 14 | if playback.CurrentFrame > playback.EndFrame then 15 | playback.CurrentFrame = 0 16 | playback.StartFrame = 0 17 | playback.Timer = 0 18 | end 19 | 20 | for entity, keyframes in pairs(SMH.KeyframeData.Players[player].Entities) do 21 | if entity ~= player then 22 | for name, mod in pairs(SMH.Modifiers) do 23 | local prevKeyframe, nextKeyframe, _ = SMH.GetClosestKeyframes(keyframes, playback.CurrentFrame, false, name) 24 | 25 | if not prevKeyframe then continue end 26 | 27 | if prevKeyframe.Frame == nextKeyframe.Frame then 28 | if prevKeyframe.Modifiers[name] and nextKeyframe.Modifiers[name] then 29 | mod:Load(entity, prevKeyframe.Modifiers[name], settings); 30 | end 31 | else 32 | local lerpMultiplier = ((playback.Timer + playback.StartFrame * timePerFrame) - prevKeyframe.Frame * timePerFrame) / ((nextKeyframe.Frame - prevKeyframe.Frame) * timePerFrame) 33 | lerpMultiplier = math.EaseInOut(lerpMultiplier, prevKeyframe.EaseOut[name], nextKeyframe.EaseIn[name]) 34 | 35 | if prevKeyframe.Modifiers[name] and nextKeyframe.Modifiers[name] then 36 | mod:LoadBetween(entity, prevKeyframe.Modifiers[name], nextKeyframe.Modifiers[name], lerpMultiplier, settings); 37 | end 38 | end 39 | end 40 | else 41 | if settings.EnableWorld then 42 | SMH.WorldKeyframesManager.Load(player, math.Round(playback.CurrentFrame), keyframes) 43 | end 44 | end 45 | end 46 | end 47 | 48 | function MGR.SetFrame(player, newFrame, settings) 49 | if not SMH.KeyframeData.Players[player] then 50 | return 51 | end 52 | 53 | for entity, keyframes in pairs(SMH.KeyframeData.Players[player].Entities) do 54 | if entity ~= player then 55 | for name, mod in pairs(SMH.Modifiers) do 56 | local prevKeyframe, nextKeyframe, lerpMultiplier = SMH.GetClosestKeyframes(keyframes, newFrame, false, name) 57 | if not prevKeyframe then 58 | continue 59 | end 60 | 61 | if lerpMultiplier <= 0 or settings.TweenDisable then 62 | mod:Load(entity, prevKeyframe.Modifiers[name], settings); 63 | elseif lerpMultiplier >= 1 then 64 | mod:Load(entity, nextKeyframe.Modifiers[name], settings); 65 | else 66 | mod:LoadBetween(entity, prevKeyframe.Modifiers[name], nextKeyframe.Modifiers[name], lerpMultiplier, settings); 67 | end 68 | end 69 | else 70 | if settings.EnableWorld then 71 | SMH.WorldKeyframesManager.Load(player, newFrame, keyframes) 72 | end 73 | end 74 | end 75 | end 76 | 77 | function MGR.SetFrameIgnore(player, newFrame, settings, ignored) 78 | if not SMH.KeyframeData.Players[player] then 79 | return 80 | end 81 | 82 | for entity, keyframes in pairs(SMH.KeyframeData.Players[player].Entities) do 83 | if ignored[entity] then continue end 84 | for name, mod in pairs(SMH.Modifiers) do 85 | local prevKeyframe, nextKeyframe, lerpMultiplier = SMH.GetClosestKeyframes(keyframes, newFrame, false, name) 86 | if not prevKeyframe then 87 | continue 88 | end 89 | 90 | if lerpMultiplier <= 0 or settings.TweenDisable then 91 | mod:Load(entity, prevKeyframe.Modifiers[name], settings); 92 | elseif lerpMultiplier >= 1 then 93 | mod:Load(entity, nextKeyframe.Modifiers[name], settings); 94 | else 95 | mod:LoadBetween(entity, prevKeyframe.Modifiers[name], nextKeyframe.Modifiers[name], lerpMultiplier, settings); 96 | end 97 | end 98 | end 99 | end 100 | 101 | function MGR.StartPlayback(player, startFrame, endFrame, playbackRate, settings) 102 | ActivePlaybacks[player] = { 103 | StartFrame = startFrame, 104 | EndFrame = endFrame, 105 | PlaybackRate = playbackRate, 106 | CurrentFrame = startFrame, 107 | PrevFrame = startFrame - 1, 108 | Timer = 0, 109 | Settings = settings, 110 | } 111 | MGR.SetFrame(player, startFrame, settings) 112 | end 113 | 114 | function MGR.StopPlayback(player) 115 | ActivePlaybacks[player] = nil 116 | end 117 | 118 | hook.Add("Think", "SMHPlaybackManagerThink", function() 119 | for player, playback in pairs(ActivePlaybacks) do 120 | if not playback.Settings.SmoothPlayback or playback.Settings.TweenDisable then 121 | 122 | playback.Timer = playback.Timer + FrameTime() 123 | local timePerFrame = 1 / playback.PlaybackRate 124 | 125 | if playback.Timer >= timePerFrame then 126 | 127 | playback.CurrentFrame = math.floor(playback.Timer / timePerFrame) + playback.StartFrame 128 | if playback.CurrentFrame > playback.EndFrame then 129 | playback.CurrentFrame = 0 130 | playback.StartFrame = 0 131 | playback.Timer = 0 132 | end 133 | 134 | if playback.CurrentFrame ~= playback.PrevFrame then 135 | playback.PrevFrame = playback.CurrentFrame 136 | MGR.SetFrame(player, playback.CurrentFrame, playback.Settings) 137 | end 138 | 139 | end 140 | else 141 | PlaybackSmooth(player, playback, playback.Settings) 142 | end 143 | end 144 | end) 145 | 146 | SMH.PlaybackManager = MGR 147 | -------------------------------------------------------------------------------- /lua/smh/server/properties_manager.lua: -------------------------------------------------------------------------------- 1 | SMH.Properties = { 2 | Players = {} 3 | } 4 | 5 | local usednames = {} 6 | 7 | local function GetModelName(entity) 8 | local mdl = string.Split(entity:GetModel(), "/") 9 | mdl = mdl[#mdl] 10 | 11 | return mdl 12 | end 13 | 14 | local function SetUniqueName(player, entity, name) 15 | if SMH.Properties.Players[player].Entities[entity] then 16 | usednames[player][SMH.Properties.Players[player].Entities[entity].Name] = nil -- so we won't consider our own name when sorting 17 | end 18 | 19 | for kentity, value in pairs(SMH.Properties.Players[player].Entities) do 20 | if kentity ~= entity and name == value.Name then -- if there's another entity with our name 21 | usednames[player][value.Name] = true 22 | break 23 | end 24 | end 25 | 26 | local namebase = name 27 | local num = 1 28 | 29 | if usednames[player][name] then 30 | local startPos = string.find(namebase, "%d*$") 31 | namebase = string.sub(namebase, 1, startPos - 1) 32 | end 33 | while usednames[player][name] do 34 | name = namebase .. num 35 | num = num + 1 36 | end 37 | usednames[player][name] = true 38 | return name 39 | end 40 | 41 | local function FindEntity(player) -- I use this to find entity that doesn't have recorded frames 42 | local sorting = {} 43 | 44 | for entity, _ in pairs(SMH.Properties.Players[player].Entities) do 45 | if sorting[entity] then continue end 46 | 47 | for k, value in pairs(SMH.KeyframeData.Players[player].Keyframes) do 48 | if value.Entity == entity then 49 | sorting[entity] = true 50 | break 51 | end 52 | end 53 | end 54 | 55 | for entity, _ in pairs(SMH.Properties.Players[player].Entities) do 56 | if not sorting[entity] then return entity end 57 | end 58 | 59 | return nil 60 | end 61 | 62 | hook.Add("PlayerInitialSpawn", "SMHInitPlayerProperties", function(player) 63 | SMH.Properties.Players[player] = { Entities = {}, TimelineSetting = {} } 64 | usednames[player] = {} 65 | end) 66 | 67 | hook.Add("PlayerDisconnected", "SMHDeleteProperties", function(player) 68 | SMH.Properties.Players[player] = nil 69 | usednames[player] = nil 70 | end) 71 | 72 | hook.Add("EntityRemoved", "SMHPropertiesEntityRemoved", function(entity) 73 | 74 | for _, player in pairs(player.GetAll()) do 75 | if SMH.Properties.Players[player] and SMH.Properties.Players[player].Entities and SMH.Properties.Players[player].Entities[entity] then 76 | usednames[player][SMH.Properties.Players[player].Entities[entity].Name] = nil 77 | SMH.Properties.Players[player].Entities[entity] = nil 78 | end 79 | end 80 | 81 | end) 82 | 83 | local MGR = {} 84 | 85 | function MGR.GetTimelinesInfo(player) 86 | if not SMH.Properties.Players[player] or not SMH.Properties.Players[player].TimelineSetting then return {} end 87 | 88 | local info = {} 89 | 90 | info = table.Copy(SMH.Properties.Players[player].TimelineSetting) 91 | 92 | return info 93 | end 94 | 95 | function MGR.GetAllProperties(player) 96 | if not SMH.Properties.Players[player] or not SMH.Properties.Players[player].Entities then return {} end 97 | 98 | local info = {} 99 | 100 | for entity, value in pairs(SMH.Properties.Players[player].Entities) do 101 | info[entity] = { 102 | Name = value.Name, 103 | Class = value.Class, 104 | Model = value.Model, 105 | } 106 | end 107 | 108 | return info 109 | end 110 | 111 | function MGR.GetAllEntitiesNames(player) 112 | if not SMH.Properties.Players[player] or not SMH.Properties.Players[player].Entities then return {} end 113 | 114 | local info = {} 115 | 116 | for entity, value in pairs(SMH.Properties.Players[player].Entities) do 117 | info[entity] = { 118 | Name = value.Name, 119 | } 120 | end 121 | 122 | return info 123 | end 124 | 125 | function MGR.RemoveEntity(player) 126 | if not SMH.KeyframeData.Players[player] or not SMH.KeyframeData.Players[player].Entities or not SMH.Properties.Players[player] or not SMH.Properties.Players[player].Entities then return end 127 | local entity = FindEntity(player) 128 | if entity then 129 | usednames[player][SMH.Properties.Players[player].Entities[entity].Name] = nil 130 | SMH.Properties.Players[player].Entities[entity] = nil 131 | end 132 | end 133 | 134 | function MGR.AddEntity(player, entities) 135 | if not SMH.Properties.Players[player] then 136 | SMH.Properties.Players[player] = { Entities = {}, TimelineSetting = {} } 137 | end 138 | 139 | for _, entity in ipairs(entities) do 140 | if not SMH.Properties.Players[player].Entities[entity] then 141 | if player ~= entity then 142 | local class = entity:GetClass() 143 | local model 144 | 145 | if class == "prop_effect" and IsValid(entity.AttachedEntity) then 146 | model = entity.AttachedEntity:GetModel() 147 | else 148 | model = entity:GetModel() 149 | end 150 | 151 | SMH.Properties.Players[player].Entities[entity] = { 152 | Name = SetUniqueName(player, entity, GetModelName(entity)), 153 | Class = class, 154 | Model = model, 155 | } 156 | else 157 | SMH.Properties.Players[player].Entities[entity] = { 158 | Name = SetUniqueName(player, entity, "world"), 159 | } 160 | end 161 | end 162 | usednames[player][SMH.Properties.Players[player].Entities[entity].Name] = true 163 | end 164 | end 165 | 166 | function MGR.SetName(player, entity, newname) 167 | if not SMH.Properties.Players[player] or not SMH.Properties.Players[player].Entities[entity] then return end 168 | if not newname then return end 169 | 170 | newname = SetUniqueName(player, entity, newname) 171 | SMH.Properties.Players[player].Entities[entity].Name = newname 172 | 173 | return newname 174 | end 175 | 176 | function MGR.InitTimelineSetting(player, timelineInfo) 177 | if not SMH.Properties.Players[player] then 178 | SMH.Properties.Players[player] = { Entities = {}, TimelineSetting = {} } 179 | end 180 | 181 | local timelines 182 | local timelinemods = {} 183 | 184 | if not next(timelineInfo) then 185 | timelines = 1 186 | 187 | timelinemods[1] = { KeyColor = Color(0, 200, 0) } 188 | for name, mod in pairs(SMH.Modifiers) do 189 | table.insert(timelinemods[1], name) 190 | end 191 | else 192 | timelines = timelineInfo.Timelines 193 | 194 | timelinemods = table.Copy(timelineInfo.TimelineMods) 195 | end 196 | 197 | SMH.Properties.Players[player].TimelineSetting = { 198 | Timelines = timelines, 199 | TimelineMods = timelinemods 200 | } 201 | end 202 | 203 | function MGR.SetTimelines(player, add) 204 | if not SMH.Properties.Players[player] or not SMH.Properties.Players[player].TimelineSetting then return end 205 | 206 | local timelines = SMH.Properties.Players[player].TimelineSetting.Timelines 207 | local count 208 | if add then 209 | count = timelines + 1 210 | else 211 | count = timelines - 1 212 | end 213 | 214 | if count > 10 or count < 1 then return end -- just in case 215 | 216 | if add then 217 | SMH.Properties.Players[player].TimelineSetting.TimelineMods[count] = { KeyColor = Color(0, 200, 0) } 218 | else 219 | SMH.Properties.Players[player].TimelineSetting.TimelineMods[timelines] = nil 220 | end 221 | 222 | SMH.Properties.Players[player].TimelineSetting.Timelines = count 223 | end 224 | 225 | function MGR.UpdateModifier(player, itimeline, name, state) 226 | if not SMH.Properties.Players[player] or not SMH.Properties.Players[player].TimelineSetting then return end 227 | 228 | if state then 229 | table.insert(SMH.Properties.Players[player].TimelineSetting.TimelineMods[itimeline], name) 230 | for i = 1, SMH.Properties.Players[player].TimelineSetting.Timelines do 231 | if i == itimeline then continue end 232 | table.RemoveByValue(SMH.Properties.Players[player].TimelineSetting.TimelineMods[i], name) 233 | end 234 | else 235 | table.RemoveByValue(SMH.Properties.Players[player].TimelineSetting.TimelineMods[itimeline], name) 236 | end 237 | 238 | return name 239 | end 240 | 241 | function MGR.UpdateKeyframeColor(player, color, timeline) 242 | if not SMH.Properties.Players[player] or not SMH.Properties.Players[player].TimelineSetting then return end 243 | 244 | SMH.Properties.Players[player].TimelineSetting.TimelineMods[timeline].KeyColor = color 245 | end 246 | 247 | function MGR.GetTimelines(player) 248 | if not SMH.Properties.Players[player] or not SMH.Properties.Players[player].TimelineSetting then return 1 end 249 | return SMH.Properties.Players[player].TimelineSetting.Timelines 250 | end 251 | 252 | function MGR.SetProperties(player, entity, properties) 253 | if not SMH.Properties.Players[player] or not SMH.Properties.Players[player].Entities[entity] then return end 254 | 255 | local newname = SetUniqueName(player, entity, properties.Name) 256 | SMH.Properties.Players[player].Entities[entity].Name = newname 257 | end 258 | 259 | SMH.PropertiesManager = MGR 260 | -------------------------------------------------------------------------------- /lua/smh/server/spawn_manager.lua: -------------------------------------------------------------------------------- 1 | local Active = {} 2 | local MGR = {} 3 | 4 | MGR.OffsetPos, MGR.OffsetAng, MGR.OffsetMode = {}, {}, {} 5 | 6 | MGR.OriginData = {} 7 | 8 | local function GetPosData(serializedKeyframes, model) 9 | for i, sEntity in pairs(serializedKeyframes.Entities) do 10 | local listname 11 | if not sEntity.Properties then -- in case if we load an old save without properties entities 12 | return 13 | else 14 | listname = sEntity.Properties.Name 15 | end 16 | 17 | if listname == model then 18 | if not sEntity.Properties.Class then return end 19 | local class, modelpath = sEntity.Properties.Class, sEntity.Properties.Model 20 | local data = {} 21 | 22 | for _, kframe in pairs(serializedKeyframes.Entities[i].Frames) do 23 | for name, mod in pairs(kframe.EntityData) do 24 | if not data[name] or data[name].Frame > kframe.Position then 25 | data[name] = {Modifiers = mod, Frame = kframe.Position} 26 | end 27 | end 28 | end 29 | 30 | return class, modelpath, data 31 | end 32 | end 33 | end 34 | 35 | local function GetDupeData(serializedKeyframes) 36 | local data = {} 37 | 38 | for _, kframe in pairs(serializedKeyframes.Entities[1].Frames) do 39 | for name, mod in pairs(kframe.EntityData) do 40 | if not data[name] or data[name].Frame > kframe.Position then 41 | data[name] = {Modifiers = mod, Frame = kframe.Position} 42 | end 43 | end 44 | end 45 | 46 | return data 47 | end 48 | 49 | local function SetOffset(player, modname, keyframe, pos) 50 | local mod = SMH.Modifiers[modname] 51 | 52 | local offsetpos = MGR.OffsetPos[player] or Vector(0, 0, 0) 53 | local offsetang = MGR.OffsetAng[player] or Angle(0, 0, 0) 54 | 55 | keyframe.Modifiers[modname] = mod:Offset(keyframe.Modifiers[modname], MGR.OriginData[player][modname].Modifiers, offsetpos, offsetang, pos) 56 | end 57 | 58 | local function SetDupeOffset(entity, modname, keyframe, firstkey) 59 | local mod = SMH.Modifiers[modname] 60 | 61 | keyframe.Modifiers[modname] = mod:OffsetDupe(entity, keyframe.Modifiers[modname], firstkey[modname].Modifiers) 62 | end 63 | 64 | function MGR.SetPreviewEntity(path, model, player, serializedKeyframes) 65 | if not Active[player] then return nil end 66 | local class, modelpath, data = GetPosData(serializedKeyframes, model) 67 | local neworigin = false 68 | if not class then 69 | player:ChatPrint("Stop Motion Helper: Failed to get entity info. Probably you're trying to load world entity, or the save is from older SMH version!") 70 | return nil 71 | end 72 | 73 | local origindata = nil 74 | 75 | if not MGR.OriginData[player] or not MGR.OffsetMode[player] then 76 | MGR.SetOrigin(model, player, serializedKeyframes) 77 | neworigin = true 78 | end 79 | 80 | return class, modelpath, data, neworigin 81 | end 82 | 83 | function MGR.SetGhost(state, player) 84 | Active[player] = state 85 | end 86 | 87 | function MGR.Spawn(model, settings, player, serializedKeyframes) 88 | if not Active[player] then return end 89 | local class, modelpath, data = GetPosData(serializedKeyframes, model) 90 | if not class then 91 | player:ChatPrint("Stop Motion Helper: Failed to get entity info. Probably you're trying to load world entity, or the save is from older SMH version!") 92 | return 93 | end 94 | 95 | if IsValid(player) and not player:CheckLimit("smhentity") then return end 96 | 97 | if class == "prop_ragdoll" and not data["physbones"] then 98 | player:ChatPrint("Stop Motion Helper: Can't spawn the ragdoll as the save doesn't have Physical Bones modifier!") 99 | return 100 | end 101 | if not data["physbones"] and not data["position"] then 102 | player:ChatPrint("Stop Motion Helper: Can't spawn the entity as the save doesn't have Physical Bones or Position and Rotation modifiers!") 103 | return 104 | end 105 | 106 | local entity = ents.Create(class) 107 | local tracepos = nil 108 | if MGR.OffsetMode[player] then 109 | tracepos = player:GetEyeTraceNoCursor().HitPos 110 | end 111 | 112 | entity:SetModel(modelpath) 113 | entity:Spawn() 114 | 115 | player:AddCount("smhentity", entity) 116 | player:AddCleanup("smhentity", entity) 117 | 118 | undo.Create("SMH Spawned entity") 119 | undo.AddEntity(entity) 120 | undo.SetPlayer(player) 121 | undo.Finish() 122 | 123 | for name, mod in pairs(SMH.Modifiers) do 124 | if not data[name] then continue end 125 | if data[name] and MGR.OriginData[player][name] and (name == "physbones" or name == "position") then 126 | local offsetpos = MGR.OffsetPos[player] or Vector(0, 0, 0) 127 | local offsetang = MGR.OffsetAng[player] or Angle(0, 0, 0) 128 | 129 | offsetdata = mod:Offset(data[name].Modifiers, MGR.OriginData[player][name].Modifiers, offsetpos, offsetang, tracepos) 130 | mod:Load(entity, offsetdata, settings) 131 | else 132 | mod:Load(entity, data[name].Modifiers, settings) 133 | end 134 | end 135 | 136 | return entity, tracepos 137 | end 138 | 139 | function MGR.OffsetKeyframes(player, entity, offsetpos) 140 | for id, keyframe in pairs(SMH.KeyframeData.Players[player].Entities[entity]) do 141 | local hasphysics = keyframe.Modifiers["physbones"] and true or false 142 | local hasposition = keyframe.Modifiers["position"] and true or false 143 | 144 | if not hasphysics and not hasposition then continue end 145 | 146 | if hasphysics then 147 | SetOffset(player, "physbones", keyframe, offsetpos) 148 | end 149 | 150 | if hasposition then 151 | SetOffset(player, "position", keyframe, offsetpos) 152 | end 153 | end 154 | end 155 | 156 | function MGR.DupeOffsetKeyframes(player, entity, serializedKeyframes) 157 | local originData = GetDupeData(serializedKeyframes) 158 | 159 | for id, keyframe in pairs(SMH.KeyframeData.Players[player].Entities[entity]) do 160 | local hasphysics = keyframe.Modifiers["physbones"] and true or false 161 | local hasposition = keyframe.Modifiers["position"] and true or false 162 | 163 | if not hasphysics and not hasposition then continue end 164 | 165 | if hasphysics then 166 | SetDupeOffset(entity, "physbones", keyframe, originData) 167 | end 168 | 169 | if hasposition then 170 | SetDupeOffset(entity, "position", keyframe, originData) 171 | end 172 | end 173 | end 174 | 175 | function MGR.SetOrigin(model, player, serializedKeyframes) 176 | local class, modelpath, data = GetPosData(serializedKeyframes, model) 177 | if not class then 178 | player:ChatPrint("Stop Motion Helper: Failed to get entity info. Probably you're trying to load world entity, or the save is from older SMH version!") 179 | return nil 180 | end 181 | 182 | MGR.OriginData[player] = data 183 | return data 184 | end 185 | 186 | function MGR.SpawnReset(player) 187 | MGR.OriginData[player] = nil 188 | end 189 | 190 | function MGR.SetOffsetMode(set, player) 191 | MGR.OffsetMode[player] = set 192 | end 193 | 194 | function MGR.SetPosOffset(pos, player) 195 | MGR.OffsetPos[player] = pos 196 | end 197 | 198 | function MGR.SetAngleOffset(ang, player) 199 | MGR.OffsetAng[player] = ang 200 | end 201 | 202 | function MGR.Pack(entities, serializedKeyframes) 203 | for _, data in ipairs(serializedKeyframes.Entities) do 204 | local entity = entities[data.Properties.Name] 205 | if not IsValid(entity) or entity:IsPlayer() then continue end 206 | 207 | duplicator.ClearEntityModifier(ent, "SMHPackage") 208 | duplicator.StoreEntityModifier(entity, "SMHPackage", table.Copy(data)) 209 | end 210 | end 211 | 212 | SMH.Spawner = MGR 213 | -------------------------------------------------------------------------------- /lua/smh/server/worldkeyframes_manager.lua: -------------------------------------------------------------------------------- 1 | local KeyboardKeys = {} 2 | do 3 | local Keys = {"0","1","2","3","4","5","6","7","8","9","a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z","Numpad_0","Numpad_1","Numpad_2","Numpad_3","Numpad_4","Numpad_5","Numpad_6","Numpad_7","Numpad_8","Numpad_9","Numpad_/","Numpad_*","Numpad_-","Numpad_+","Numpad_Enter","Numpad_.","[","]","SEMICOLON","'","`",",",".","/","\\","-","=","ENTER","SPACE","BACKSPACE","TAB","CAPSLOCK","NUMLOCK","ESCAPE","SCROLLLOCK","INS","DEL","HOME","END","PGUP","PGDN","PAUSE","SHIFT","RSHIFT","ALT","RALT","CTRL","RCTRL","LWIN","RWIN","APP","UPARROW","LEFTARROW","DOWNARROW","RIGHTARROW","F1","F2","F3","F4","F5","F6","F7","F8","F9","F10","F11","F12","CAPSLOCKTOGGLE","NUMLOCKTOGGLE","SCROLLLOCKTOGGLE"} 4 | for id, key in ipairs(Keys) do 5 | KeyboardKeys[key] = id 6 | end 7 | end 8 | 9 | local LastFrame = -1 10 | 11 | local function GetKeys(str) 12 | local split = string.Split(str, " ") 13 | local result = {} 14 | 15 | for _, key in ipairs(split) do 16 | if KeyboardKeys[key] then 17 | table.insert(result, KeyboardKeys[key]) 18 | end 19 | end 20 | 21 | return result 22 | end 23 | 24 | local MGR = {} 25 | 26 | function MGR.Load(player, frame, keyframes) 27 | if LastFrame == frame then return end 28 | 29 | for _, keyframe in pairs(keyframes) do 30 | if keyframe.Frame == frame then 31 | if keyframe.Modifiers["world"].Console ~= "" then 32 | player:ConCommand(keyframe.Modifiers["world"].Console) 33 | end 34 | 35 | local PushKeys = GetKeys(keyframe.Modifiers["world"].Push) 36 | for _, key in ipairs(PushKeys) do 37 | numpad.Activate(player, key, true) 38 | end 39 | 40 | local ReleaseKeys = GetKeys(keyframe.Modifiers["world"].Release) 41 | for _, key in ipairs(ReleaseKeys) do 42 | numpad.Deactivate(player, key, true) 43 | end 44 | break 45 | end 46 | end 47 | end 48 | 49 | SMH.WorldKeyframesManager = MGR 50 | -------------------------------------------------------------------------------- /lua/smh/shared.lua: -------------------------------------------------------------------------------- 1 | if not SMH then 2 | SMH = {} 3 | end 4 | 5 | SMH.MessageTypes = { 6 | "SetFrame", 7 | "SetFrameResponse", 8 | 9 | "SelectEntity", 10 | "SelectEntityResponse", 11 | 12 | "CreateKeyframe", 13 | "UpdateKeyframe", 14 | "UpdateKeyframeExecute", 15 | "CopyKeyframe", 16 | "CopyKeyframeExecute", 17 | "UpdateKeyframeResponse", 18 | "DeleteKeyframe", 19 | "DeleteKeyframeResponse", 20 | "GetAllKeyframes", 21 | 22 | "StartPlayback", 23 | "StopPlayback", 24 | "PlaybackResponse", 25 | 26 | "SetRendering", 27 | "UpdateGhostState", 28 | "UpdateGhostStateResponse", 29 | 30 | "GetServerSaves", 31 | "GetServerSavesResponse", 32 | "GetModelList", 33 | "GetModelListResponse", 34 | "GetModelInfo", 35 | "GetModelInfoResponse", 36 | "GetServerEntities", 37 | "GetServerEntitiesResponse", 38 | "Load", 39 | "LoadResponse", 40 | "RequestSave", 41 | "SaveExists", 42 | "Save", 43 | "SaveResponse", 44 | "AddFolderResponse", 45 | "RequestGoToFolder", 46 | "RequestAppend", 47 | "RequestAppendResponse", 48 | "Append", 49 | "RequestPack", 50 | "DeleteSave", 51 | "DeleteSaveResponse", 52 | 53 | "ApplyEntityName", 54 | "ApplyEntityNameResponse", 55 | "UpdateTimeline", 56 | "UpdateTimelineResponse", 57 | "RequestModifiers", 58 | "RequestModifiersResponse", 59 | "AddTimeline", 60 | "RemoveTimeline", 61 | "UpdateTimelineInfoResponse", 62 | "UpdateModifier", 63 | "UpdateModifierResponse", 64 | "UpdateKeyframeColor", 65 | "UpdateKeyframeColorResponse", 66 | 67 | "SetPreviewEntity", 68 | "SetSpawnGhost", 69 | "SpawnEntity", 70 | "SpawnReset", 71 | "SetSpawnOffsetMode", 72 | "SetSpawnOrigin", 73 | "OffsetPos", 74 | "OffsetAng", 75 | 76 | "SetTimeline", 77 | "RequestTimelineInfo", 78 | "RequestTimelineInfoResponse", 79 | 80 | "RequestWorldData", 81 | "RequestWorldDataResponse", 82 | "UpdateWorld", 83 | 84 | "StartPhysicsRecord", 85 | "StopPhysicsRecord", 86 | "StopPhysicsRecordResponse", 87 | } 88 | for key, val in pairs(SMH.MessageTypes) do 89 | local prefixVal = "SMH" .. val 90 | SMH.MessageTypes[val] = prefixVal 91 | end 92 | 93 | cleanup.Register("smhentity") 94 | CreateConVar("sbox_maxsmhentity", 20, FCVAR_NOTIFY) 95 | 96 | include("shared/saves.lua") 97 | include("shared/tablesplit.lua") 98 | -------------------------------------------------------------------------------- /lua/smh/shared/saves.lua: -------------------------------------------------------------------------------- 1 | local function GetModelName(entity) 2 | local mdl = string.Split(entity:GetModel(), "/"); 3 | mdl = mdl[#mdl]; 4 | 5 | return mdl 6 | end 7 | 8 | local function SetUniqueName(name, usedModelNames) 9 | local namebase = name 10 | local num = 1 11 | 12 | if usedModelNames[name] then 13 | local startPos = string.find(namebase, "%d*$") 14 | namebase = string.sub(namebase, 1, startPos - 1) 15 | end 16 | while usedModelNames[name] do 17 | name = namebase .. num 18 | num = num + 1 19 | end 20 | usedModelNames[name] = true 21 | return name 22 | end 23 | 24 | local function ProcessKeyframes(keyframes, entityMappedKeyframes, properties, player) 25 | for _, keyframe in pairs(keyframes) do 26 | local entity = keyframe.Entity 27 | if not IsValid(entity) then 28 | continue 29 | end 30 | 31 | if entity ~= player then 32 | if not entityMappedKeyframes[entity] then 33 | local mdl = GetModelName(entity) 34 | 35 | entityMappedKeyframes[entity] = { 36 | Model = mdl, 37 | Properties = { 38 | Name = properties[entity].Name, 39 | Class = properties[entity].Class, 40 | Model = properties[entity].Model, 41 | }, 42 | Frames = {}, 43 | } 44 | end 45 | else 46 | if not entityMappedKeyframes[entity] then 47 | local mdl = "world" 48 | 49 | entityMappedKeyframes[entity] = { 50 | Model = mdl, 51 | Properties = { 52 | Name = properties[entity].Name, 53 | IsWorld = true, 54 | }, 55 | Frames = {}, 56 | } 57 | end 58 | end 59 | table.insert(entityMappedKeyframes[entity].Frames, { 60 | Position = keyframe.Frame, 61 | EaseIn = table.Copy(keyframe.EaseIn), 62 | EaseOut = table.Copy(keyframe.EaseOut), 63 | EntityData = table.Copy(keyframe.Modifiers), 64 | }) 65 | end 66 | end 67 | 68 | local SaveDir = "smh/" 69 | local SettingsDir = "smhsettings/" 70 | local PlayerPath = {} 71 | 72 | local MGR = {} 73 | 74 | function MGR.ListFiles(player) 75 | local path = SaveDir .. (PlayerPath[player] or "") 76 | local files, _ = file.Find(path .. "*.txt", "DATA") 77 | local _, dirs = file.Find(path .. "*", "DATA") 78 | 79 | local saves = {} 80 | for _, file in pairs(files) do 81 | table.insert(saves, file:sub(1, -5)) 82 | end 83 | 84 | return dirs, saves, path 85 | end 86 | 87 | function MGR.ListSettings() 88 | local files, dirs = file.Find(SettingsDir .. "*.txt", "DATA") 89 | 90 | local settings = {} 91 | for _, setting in pairs(files) do 92 | table.insert(settings, setting:sub(1, -5)) 93 | end 94 | 95 | return settings 96 | end 97 | 98 | function MGR.Load(path, player) 99 | path = SaveDir .. (PlayerPath[player] or "") .. path .. ".txt" 100 | if not file.Exists(path, "DATA") then 101 | error("SMH file does not exist: " .. path) 102 | end 103 | 104 | local json = file.Read(path) 105 | local serializedKeyframes = util.JSONToTable(json) 106 | if not serializedKeyframes then 107 | error("SMH file load failure") 108 | end 109 | 110 | return serializedKeyframes 111 | end 112 | 113 | function MGR.ListModels(path, player) 114 | local serializedKeyframes = MGR.Load(path, player) 115 | local models = {} 116 | local map = serializedKeyframes.Map 117 | local listname = " " 118 | for _, sEntity in pairs(serializedKeyframes.Entities) do 119 | if not sEntity.Properties then -- in case if we load an old save without properties entities 120 | listname = sEntity.Model 121 | else 122 | listname = sEntity.Properties.Name 123 | end 124 | table.insert(models, listname) 125 | end 126 | return models, map 127 | end 128 | 129 | function MGR.GetModelName(path, modelName, player) 130 | local serializedKeyframes = MGR.Load(path, player) 131 | 132 | for _, sEntity in pairs(serializedKeyframes.Entities) do 133 | if sEntity.Properties then 134 | if sEntity.Properties.Name == modelName then 135 | local model, class 136 | model = sEntity.Properties.Model 137 | 138 | if not sEntity.Properties.Class then 139 | class = "Error: No class found" 140 | else 141 | class = sEntity.Properties.Class 142 | end 143 | return model, class 144 | end 145 | else 146 | if sEntity.Model == modelName then 147 | return sEntity.Model, "Error: No class found" 148 | end 149 | end 150 | end 151 | return "Error: No model found", "Error: No class found" 152 | end 153 | 154 | function MGR.LoadForEntity(path, modelName, player) 155 | local serializedKeyframes = MGR.Load(path, player) 156 | for _, sEntity in pairs(serializedKeyframes.Entities) do 157 | if not sEntity.Properties then 158 | if sEntity.Model == modelName then 159 | 160 | sEntity.Properties = { 161 | Name = sEntity.Model, 162 | } 163 | 164 | return sEntity.Frames, sEntity.Properties 165 | end 166 | else 167 | if sEntity.Properties.Name == modelName then 168 | return sEntity.Frames, sEntity.Properties, sEntity.Properties.IsWorld 169 | end 170 | end 171 | end 172 | return nil 173 | end 174 | 175 | function MGR.Serialize(keyframes, properties, player) 176 | local entityMappedKeyframes = {} 177 | 178 | ProcessKeyframes(keyframes, entityMappedKeyframes, properties, player) 179 | 180 | local serializedKeyframes = { 181 | Map = game.GetMap(), 182 | Entities = {}, 183 | } 184 | 185 | for _, skf in pairs(entityMappedKeyframes) do 186 | table.insert(serializedKeyframes.Entities, skf) 187 | end 188 | 189 | return serializedKeyframes 190 | end 191 | 192 | function MGR.CheckIfExists(path, player) 193 | if not file.Exists(SaveDir, "DATA") or not file.IsDir(SaveDir, "DATA") then 194 | file.CreateDir(SaveDir) 195 | end 196 | 197 | path = SaveDir .. (PlayerPath[player] or "") .. path .. ".txt" 198 | if file.Exists(path, "DATA") and not file.IsDir(path, "DATA") then return true end 199 | 200 | return false 201 | end 202 | 203 | function MGR.GetUnusedNames(path, properties, player) 204 | if not file.Exists(SaveDir, "DATA") or not file.IsDir(SaveDir, "DATA") then 205 | file.CreateDir(SaveDir) 206 | end 207 | 208 | local serializedKeyframes = MGR.Load(path, player) 209 | local entityNames = {} 210 | 211 | for _, data in ipairs(serializedKeyframes.Entities) do 212 | if data.Properties then 213 | entityNames[data.Properties.Name] = true 214 | else 215 | entityNames[data.Model] = true 216 | end 217 | end 218 | 219 | for entity, data in pairs(properties) do 220 | entityNames[data.Name] = nil 221 | end 222 | 223 | return entityNames 224 | end 225 | 226 | function MGR.SerializeAndAppend(path, keyframes, properties, player, saveNames, gameNames) 227 | if not file.Exists(SaveDir, "DATA") or not file.IsDir(SaveDir, "DATA") then 228 | file.CreateDir(SaveDir) 229 | end 230 | 231 | local oldSerializedKeyframes = MGR.Load(path, player) 232 | local usedModelNames, entityMappedKeyframes = {}, {} 233 | local serializedKeyframes = { 234 | Map = game.GetMap(), 235 | Entities = {}, 236 | } 237 | 238 | for _, data in ipairs(oldSerializedKeyframes.Entities) do 239 | if data.Properties then 240 | if saveNames[data.Properties.Name] then 241 | table.insert(serializedKeyframes.Entities, data) 242 | end 243 | else 244 | if saveNames[data.Model] then 245 | table.insert(serializedKeyframes.Entities, data) 246 | end 247 | end 248 | end 249 | 250 | for name, _ in pairs(saveNames) do 251 | usedModelNames[name] = true 252 | end 253 | 254 | ProcessKeyframes(keyframes, entityMappedKeyframes, properties, player) 255 | 256 | for _, skf in pairs(entityMappedKeyframes) do 257 | if gameNames[skf.Properties.Name] then 258 | skf.Properties.Name = SetUniqueName(skf.Properties.Name, usedModelNames) 259 | table.insert(serializedKeyframes.Entities, skf) 260 | end 261 | end 262 | 263 | return serializedKeyframes 264 | end 265 | 266 | function MGR.Save(path, serializedKeyframes, player) 267 | if not file.Exists(SaveDir, "DATA") or not file.IsDir(SaveDir, "DATA") then 268 | file.CreateDir(SaveDir) 269 | end 270 | 271 | path = SaveDir .. (PlayerPath[player] or "") .. path .. ".txt" 272 | local json = util.TableToJSON(serializedKeyframes) 273 | file.Write(path, json) 274 | end 275 | 276 | function MGR.AddFolder(path, player) 277 | local fullpath = SaveDir .. (PlayerPath[player] or "") .. path 278 | 279 | if file.Exists(fullpath, "DATA") and file.IsDir(fullpath, "DATA") then return nil end 280 | 281 | file.CreateDir(fullpath) 282 | return path 283 | end 284 | 285 | function MGR.CopyIfExists(pathFrom, pathTo, player) 286 | pathFrom = SaveDir .. (PlayerPath[player] or "") .. pathFrom .. ".txt" 287 | pathTo = SaveDir .. (PlayerPath[player] or "") .. pathTo .. ".txt" 288 | 289 | if file.Exists(pathFrom, "DATA") then 290 | file.Write(pathTo, file.Read(pathFrom)); 291 | end 292 | end 293 | 294 | function MGR.Delete(path, player) 295 | path = SaveDir .. (PlayerPath[player] or "") .. path .. ".txt" 296 | if file.Exists(path, "DATA") then 297 | file.Delete(path) 298 | end 299 | end 300 | 301 | function MGR.DeleteFolder(path, player) 302 | path = SaveDir .. (PlayerPath[player] or "") .. path 303 | if file.Exists(path, "DATA") and file.IsDir(path, "DATA") then 304 | file.Delete(path) 305 | end 306 | 307 | if file.Exists(path, "DATA") and file.IsDir(path, "DATA") then return false end 308 | return true 309 | end 310 | 311 | function MGR.SaveProperties(timeline, name) 312 | if next(timeline) == nil then return end 313 | 314 | if not file.Exists(SettingsDir, "DATA") or not file.IsDir(SettingsDir, "DATA") then 315 | file.CreateDir(SettingsDir) 316 | end 317 | 318 | local template = { 319 | Timelines = timeline.Timelines, 320 | TimelineMods = table.Copy(timeline.TimelineMods), 321 | } 322 | 323 | path = SettingsDir .. name .. ".txt" 324 | local json = util.TableToJSON(template) 325 | file.Write(path, json) 326 | end 327 | 328 | function MGR.GetPreferences(name) 329 | path = SettingsDir .. name .. ".txt" 330 | if not file.Exists(path, "DATA") then return nil end 331 | 332 | local json = file.Read(path) 333 | local template = util.JSONToTable(json) 334 | if not template then 335 | error("SMH settings file load failure") 336 | end 337 | 338 | for i = 1, template.Timelines do 339 | local color = template.TimelineMods[i].KeyColor 340 | template.TimelineMods[i].KeyColor = Color(color.r, color.g, color.b) 341 | end 342 | return template 343 | end 344 | 345 | function MGR.GetPath(player) 346 | if not PlayerPath[player] then 347 | PlayerPath[player] = "" 348 | end 349 | 350 | return PlayerPath[player] 351 | end 352 | 353 | function MGR.GoBackPath(player) 354 | if not PlayerPath[player] or PlayerPath[player] == "" then 355 | return 356 | end 357 | 358 | local kablooey = string.Explode("/", PlayerPath[player]) 359 | 360 | if #kablooey > 2 then 361 | PlayerPath[player] = table.concat(kablooey, "/", 1, #kablooey - 2) .. "/" 362 | else 363 | PlayerPath[player] = "" 364 | end 365 | end 366 | 367 | function MGR.SetPath(path, player) 368 | PlayerPath[player] = path 369 | end 370 | 371 | SMH.Saves = MGR 372 | -------------------------------------------------------------------------------- /lua/smh/shared/tablesplit.lua: -------------------------------------------------------------------------------- 1 | local keyframesAssembling = {} 2 | local timelineAssembling = {} 3 | local listAssembling = {} 4 | 5 | local MGR = {} -- btw D stands for "deconstruct", A for "Assemble" 6 | 7 | function MGR.DKeyframes(keyframes) 8 | local IDs, ents, Frame, In, Out, ModCount, Modifiers = {}, {}, {}, {}, {}, {}, {} 9 | local i = 0 10 | for _, keyframe in pairs(keyframes) do 11 | i = i + 1 12 | 13 | IDs[i] = keyframe.ID 14 | Frame[i] = keyframe.Frame 15 | ents[i] = keyframe.Entity 16 | Modifiers[i], In[i], Out[i] = {}, {}, {} 17 | ModCount[i] = 0 18 | for name, data in pairs(keyframe.Modifiers) do 19 | ModCount[i] = ModCount[i] + 1 20 | Modifiers[i][ModCount[i]] = name 21 | In[i][ModCount[i]] = keyframe.EaseIn[name] 22 | Out[i][ModCount[i]] = keyframe.EaseOut[name] 23 | end 24 | end 25 | 26 | return i, IDs, ents, Frame, In, Out, ModCount, Modifiers 27 | end 28 | 29 | function MGR.AKeyframes(ID, entity, Frame, In, Out, Modifiers) 30 | local keyframe = {} 31 | keyframe.ID = ID 32 | keyframe.Entity = entity 33 | keyframe.Frame = Frame 34 | keyframe.EaseIn = table.Copy(In) 35 | keyframe.EaseOut = table.Copy(Out) 36 | keyframe.Modifiers = table.Copy(Modifiers) 37 | 38 | table.insert(keyframesAssembling, keyframe) 39 | end 40 | 41 | function MGR.GetKeyframes() 42 | local keyframes = table.Copy(keyframesAssembling) 43 | keyframesAssembling = {} 44 | return keyframes 45 | end 46 | 47 | function MGR.DProperties(timeline) 48 | if not next(timeline) then return end 49 | local Timelines = timeline.Timelines 50 | local KeyColor, Modifiers, ModCount = {}, {}, {} 51 | 52 | for key, _ in ipairs(timeline.TimelineMods) do 53 | Modifiers[key] = {} 54 | ModCount[key] = #timeline.TimelineMods[key] 55 | 56 | for k, value in pairs(timeline.TimelineMods[key]) do 57 | if k == "KeyColor" then 58 | KeyColor[key] = value 59 | continue 60 | end 61 | 62 | Modifiers[key][k] = value 63 | end 64 | end 65 | return Timelines, KeyColor, ModCount, Modifiers 66 | end 67 | 68 | function MGR.StartAProperties(Timelines) 69 | timelineAssembling.Timelines = Timelines 70 | timelineAssembling.TimelineMods = {} 71 | return Timelines 72 | end 73 | 74 | function MGR.AProperties(Timeline, Modifier, KeyColor) 75 | if not timelineAssembling.TimelineMods[Timeline] then 76 | timelineAssembling.TimelineMods[Timeline] = {} 77 | end 78 | if KeyColor then 79 | timelineAssembling.TimelineMods[Timeline].KeyColor = KeyColor 80 | end 81 | if Modifier then 82 | table.insert(timelineAssembling.TimelineMods[Timeline], Modifier) 83 | end 84 | end 85 | 86 | function MGR.GetProperties() 87 | local timeline = table.Copy(timelineAssembling) 88 | timelineAssembling = {} 89 | return timeline 90 | end 91 | 92 | function MGR.DTable(list) 93 | local items, keys, count = {}, {}, 0 94 | for key, item in pairs(list) do 95 | table.insert(items, item) 96 | table.insert(keys, key) 97 | count = count + 1 98 | end 99 | return items, keys, count 100 | end 101 | 102 | function MGR.ATable(key, item) 103 | if not tonumber(key) then 104 | listAssembling[key] = item 105 | else 106 | listAssembling[tonumber(key)] = item 107 | end 108 | end 109 | 110 | function MGR.GetTable() 111 | local list = table.Copy(listAssembling) 112 | listAssembling = {} 113 | return list 114 | end 115 | 116 | SMH.TableSplit = MGR 117 | -------------------------------------------------------------------------------- /workshop_export.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Copy files to another folder that can be published to workshop 4 | 5 | INPUT="$1" 6 | OUTPUT="$2" 7 | 8 | # Make output directory 9 | mkdir -p "$OUTPUT" 10 | 11 | # Copy all files to output 12 | cp -r "$INPUT/" "$OUTPUT/" 13 | 14 | # Remove non-lua files from output 15 | find "$OUTPUT" -type f ! -name '*.lua' -delete 16 | 17 | # Copy addon.json 18 | cp "$INPUT/addon.json" "$OUTPUT" 19 | 20 | echo "All done!" --------------------------------------------------------------------------------