├── .gitignore ├── models ├── vrmod │ ├── tpbeam.mdl │ ├── tpbeam.vvd │ ├── tpbeam.dx80.vtx │ └── tpbeam.dx90.vtx └── player │ ├── vr_hands.mdl │ ├── vr_hands.phy │ ├── vr_hands.vvd │ ├── vr_hands.dx80.vtx │ └── vr_hands.dx90.vtx ├── materials ├── vrmod │ ├── tpbeam.vtf │ └── tpbeam.vmt └── models │ └── vr_hands │ ├── c_arms_citizen_hands.vtf │ └── c_arms_citizen_hands.vmt ├── addon.json ├── lua ├── weapons │ └── weapon_vrmod_empty.lua ├── vrmod │ ├── player │ │ ├── sh_pmchange.lua │ │ ├── sh_flashlight.lua │ │ ├── cl_laser_pointer.lua │ │ └── cl_character_hands.lua │ ├── utils │ │ ├── sh_math.lua │ │ ├── sh_weps.lua │ │ ├── cl_rendering.lua │ │ ├── sh_trace.lua │ │ ├── sh_frames.lua │ │ └── sh_vehicles.lua │ ├── input │ │ ├── sh_buttons.lua │ │ ├── cl_seated.lua │ │ ├── sh_gravity_and_physgun.lua │ │ ├── cl_keypad.lua │ │ ├── sh_doors.lua │ │ ├── sh_locomotion.lua │ │ └── sh_glide.lua │ ├── ui │ │ ├── cl_dermapopups.lua │ │ ├── cl_quickmenu.lua │ │ ├── cl_buttons.lua │ │ ├── cl_halos.lua │ │ ├── cl_worldtips.lua │ │ ├── cl_actioneditor.lua │ │ ├── cl_hud.lua │ │ ├── sh_numpad.lua │ │ ├── cl_heightadjust.lua │ │ ├── cl_mapbrowser.lua │ │ └── cl_ui.lua │ ├── api │ │ └── sh_api.lua │ ├── loader.lua │ ├── pickup │ │ ├── sh_manualpickup.lua │ │ ├── sh_pickup_arcvr.lua │ │ ├── sh_dropweapon.lua │ │ ├── sv_weaponreplacer.lua │ │ └── sh_pickup.lua │ ├── physics │ │ ├── sh_collisions.lua │ │ └── sv_physhands.lua │ ├── logger.lua │ └── core │ │ └── sh_startup.lua └── autorun │ └── vrmod_init.lua ├── .github └── FUNDING.yml ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/settings.json 2 | -------------------------------------------------------------------------------- /models/vrmod/tpbeam.mdl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abyss-c0re/vrmod-x64/HEAD/models/vrmod/tpbeam.mdl -------------------------------------------------------------------------------- /models/vrmod/tpbeam.vvd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abyss-c0re/vrmod-x64/HEAD/models/vrmod/tpbeam.vvd -------------------------------------------------------------------------------- /materials/vrmod/tpbeam.vtf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abyss-c0re/vrmod-x64/HEAD/materials/vrmod/tpbeam.vtf -------------------------------------------------------------------------------- /models/player/vr_hands.mdl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abyss-c0re/vrmod-x64/HEAD/models/player/vr_hands.mdl -------------------------------------------------------------------------------- /models/player/vr_hands.phy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abyss-c0re/vrmod-x64/HEAD/models/player/vr_hands.phy -------------------------------------------------------------------------------- /models/player/vr_hands.vvd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abyss-c0re/vrmod-x64/HEAD/models/player/vr_hands.vvd -------------------------------------------------------------------------------- /models/vrmod/tpbeam.dx80.vtx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abyss-c0re/vrmod-x64/HEAD/models/vrmod/tpbeam.dx80.vtx -------------------------------------------------------------------------------- /models/vrmod/tpbeam.dx90.vtx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abyss-c0re/vrmod-x64/HEAD/models/vrmod/tpbeam.dx90.vtx -------------------------------------------------------------------------------- /models/player/vr_hands.dx80.vtx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abyss-c0re/vrmod-x64/HEAD/models/player/vr_hands.dx80.vtx -------------------------------------------------------------------------------- /models/player/vr_hands.dx90.vtx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abyss-c0re/vrmod-x64/HEAD/models/player/vr_hands.dx90.vtx -------------------------------------------------------------------------------- /materials/models/vr_hands/c_arms_citizen_hands.vtf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abyss-c0re/vrmod-x64/HEAD/materials/models/vr_hands/c_arms_citizen_hands.vtf -------------------------------------------------------------------------------- /addon.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "VRMod_x64", 3 | "type": "tool", 4 | "tags": ["realism"], 5 | "ignore": [ 6 | ".git", 7 | "LICENSE", 8 | "README.md", 9 | "models/**/*.sw.vtx", 10 | "materials/**/*.sw.vtx", 11 | "**/*.sw.vtx", 12 | ".vscode/settings.json" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /materials/vrmod/tpbeam.vmt: -------------------------------------------------------------------------------- 1 | "UnlitGeneric" 2 | { 3 | "$basetexture" "vrmod/tpbeam" 4 | //"$vertexcolor" 1 5 | "$translucent" 1 6 | "$model" 1 7 | 8 | "Proxies" 9 | { 10 | "TextureScroll" 11 | { 12 | "texturescrollvar" "$baseTextureTransform" 13 | "texturescrollrate" 3 14 | "texturescrollangle" 270 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /lua/weapons/weapon_vrmod_empty.lua: -------------------------------------------------------------------------------- 1 | SWEP.PrintName = "Empty Hand" 2 | SWEP.Slot = 0 3 | SWEP.SlotPos = 0 4 | SWEP.Spawnable = true 5 | SWEP.ViewModel = "models/weapons/c_arms.mdl" 6 | SWEP.WorldModel = "" 7 | SWEP.DrawAmmo = false 8 | SWEP.Primary.Ammo = "none" 9 | SWEP.Primary.ClipSize = 0 10 | SWEP.Primary.DefaultClip = 0 11 | SWEP.Secondary.Ammo = "none" 12 | SWEP.Secondary.ClipSize = -1 13 | SWEP.Secondary.DefaultClip = -1 14 | function SWEP:Initialize() 15 | self:SetHoldType("fist") 16 | end 17 | 18 | function SWEP:PrimaryAttack() 19 | end 20 | 21 | function SWEP:SecondaryAttack() 22 | end -------------------------------------------------------------------------------- /materials/models/vr_hands/c_arms_citizen_hands.vmt: -------------------------------------------------------------------------------- 1 | "VertexLitGeneric" 2 | { 3 | "$basetexture" "models/vr_hands/c_arms_citizen_hands" 4 | "$bumpmap" "models/weapons/c_arms_citizen/c_arms_citizen_hands_normal" 5 | 6 | "$phong" "1" 7 | "$phongboost" "1.0" 8 | "$phongexponent" "8" 9 | "$phongfresnelranges" "[.1 .5 1]" 10 | 11 | "$detail" "models/weapons/c_arms_citizen/skin_detail" 12 | "$detailscale" "7" 13 | "$detailblendfactor" "0.4" 14 | "$detailblendmode" "0" 15 | 16 | "$phongtint" "[0.8 0.6 0.4]" 17 | 18 | "$rimlight" "1" 19 | "$rimlightboost" "0.2" 20 | "$rimlightexponent" "4" 21 | } -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [Abyss-c0re] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username 14 | thanks_dev: # Replace with a single thanks.dev username 15 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Altered source versionn Copyright (c) 2025 Abyss-c0re 2 | 3 | Original Software Copyright (c) 2019 Catse 4 | 5 | This software is provided 'as-is', without any express or implied 6 | warranty. In no event will the authors be held liable for any damages 7 | arising from the use of this software. 8 | 9 | Permission is granted to anyone to use this software for any purpose, 10 | including commercial applications, and to alter it and redistribute it 11 | freely, subject to the following restrictions: 12 | 13 | 1. The origin of this software must not be misrepresented; you must not 14 | claim that you wrote the original software. If you use this software 15 | in a product, an acknowledgment in the product documentation would be 16 | appreciated but is not required. 17 | 2. Altered source versions must be plainly marked as such, and must not be 18 | misrepresented as being the original software. 19 | 3. This notice may not be removed or altered from any source distribution. 20 | -------------------------------------------------------------------------------- /lua/vrmod/player/sh_pmchange.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | 3 | this is a shitty temporary workaround for some addons breaking player:GetModel() on the client 4 | 5 | for example, clear a pac3 outfit that changed your player model, then change your pm normally using the default gmod player model selector 6 | and player:GetModel() will always return the old model no matter how many times you change pm 7 | 8 | --]] 9 | local cv_allowpmchg = CreateClientConVar("vrmod_pmchange", 1, true, FCVAR_ARCHIVE) 10 | if CLIENT then 11 | if cv_allowpmchg:GetBool() then 12 | net.Receive("vrmod_pmchange", function() 13 | local ply = player.GetBySteamID(net.ReadString()) 14 | local model = net.ReadString() 15 | if ply then ply.vrmod_pm = model end 16 | end) 17 | elseif SERVER then 18 | if cv_allowpmchg:GetBool() then 19 | util.AddNetworkString("vrmod_pmchange") 20 | hook.Add("InitPostEntity", "vrmod_pmchange", function() 21 | local og = getmetatable(Entity(0)).SetModel 22 | getmetatable(Entity(0)).SetModel = function(...) 23 | local args = {...} 24 | og(unpack(args)) 25 | if args[1]:IsPlayer() then 26 | net.Start("vrmod_pmchange") 27 | net.WriteString(args[1]:SteamID()) 28 | net.WriteString(args[2]) 29 | net.Broadcast() 30 | end 31 | end 32 | end) 33 | end 34 | end 35 | end -------------------------------------------------------------------------------- /lua/vrmod/utils/sh_math.lua: -------------------------------------------------------------------------------- 1 | g_VR = g_VR or {} 2 | vrmod = vrmod or {} 3 | vrmod.utils = vrmod.utils or {} 4 | 5 | function vrmod.utils.VecAlmostEqual(v1, v2, threshold) 6 | if not v1 or not v2 then return false end 7 | return v1:DistToSqr(v2) < (threshold or 0.05) ^ 2 8 | end 9 | 10 | function vrmod.utils.AngAlmostEqual(a1, a2, threshold) 11 | if not a1 or not a2 then return false end 12 | threshold = threshold or 0.5 -- degrees 13 | return math.abs(math.AngleDifference(a1.p, a2.p)) < threshold and math.abs(math.AngleDifference(a1.y, a2.y)) < threshold and math.abs(math.AngleDifference(a1.r, a2.r)) < threshold 14 | end 15 | 16 | function vrmod.utils.LengthSqr(v) 17 | return v.x * v.x + v.y * v.y + v.z * v.z 18 | end 19 | 20 | function vrmod.utils.SubVec(a, b) 21 | return Vector(a.x - b.x, a.y - b.y, a.z - b.z) 22 | end 23 | 24 | function vrmod.utils.SmoothVector(current, target, smoothingFactor) 25 | return current + (target - current) * smoothingFactor 26 | end 27 | 28 | function vrmod.utils.LerpAngleWrap(factor, current, target) 29 | local diff = math.AngleDifference(target, current) -- handles ±180 wrap 30 | return current + diff * factor 31 | end 32 | 33 | function vrmod.utils.SmoothAngle(current, target, smoothingFactor) 34 | local diff = target - current 35 | diff.p = math.NormalizeAngle(diff.p) 36 | diff.y = math.NormalizeAngle(diff.y) 37 | diff.r = math.NormalizeAngle(diff.r) 38 | return current + diff * smoothingFactor 39 | end -------------------------------------------------------------------------------- /lua/autorun/vrmod_init.lua: -------------------------------------------------------------------------------- 1 | vrmod = vrmod or {} 2 | vrmod.status = { 3 | api = false, 4 | utils = false, 5 | core = false, 6 | network = false, 7 | input = false, 8 | player = false, 9 | physics = false, 10 | pickup = false, 11 | combat = false, 12 | ui = false, 13 | } 14 | 15 | local subsystemOrder = {"api", "utils", "core", "network", "input", "player", "physics", "pickup", "combat", "ui"} 16 | local baseFolder = "vrmod/" 17 | local function LoadAllSubsystems() 18 | for _, sub in ipairs(subsystemOrder) do 19 | local folderPath = baseFolder .. sub 20 | if VRMod_Loader then 21 | VRMod_Loader(folderPath) 22 | else 23 | print("[VRMod] VRMod_Loader missing for subsystem:", sub) 24 | end 25 | end 26 | end 27 | 28 | -- Fire the hook once loader.lua has completed 29 | hook.Add("VRMod_PostLoader", "LoadSubsystems", LoadAllSubsystems) 30 | -- Force include logger first 31 | if SERVER then AddCSLuaFile("vrmod/logger.lua") end 32 | include("vrmod/logger.lua") 33 | -- Then loader 34 | if SERVER then AddCSLuaFile("vrmod/loader.lua") end 35 | include("vrmod/loader.lua") 36 | hook.Run("VRMod_PostLoader") 37 | -- ConCommand to print subsystem status 38 | concommand.Add("vrmod_status", function(ply, cmd, args) 39 | print("[VRMod] Subsystem Status:") 40 | for _, sub in ipairs(subsystemOrder) do 41 | local status = vrmod.status[sub] and "[Running]" or "[Stopped]" 42 | print(string.format("%s: %s", string.upper(sub), status)) 43 | end 44 | end) -------------------------------------------------------------------------------- /lua/vrmod/input/sh_buttons.lua: -------------------------------------------------------------------------------- 1 | if SERVER then util.AddNetworkString("VRButtonPresserMessage") end 2 | -- CLIENT: Detect VR input and notify server 3 | if CLIENT then 4 | local cl_interactive_buttons = CreateClientConVar("vrmod_interactive_buttons", "1", true, FCVAR_CLIENTCMD_CAN_EXECUTE + FCVAR_ARCHIVE) 5 | hook.Add("VRMod_Input", "VRModButtonPresser", function(action, state) 6 | if not cl_interactive_buttons:GetBool() or not g_VR.active then return end 7 | -- Only act when the input is first pressed down 8 | if state then 9 | if action == "boolean_left_pickup" then 10 | net.Start("VRButtonPresserMessage") 11 | net.WriteBool(true) -- Left hand 12 | net.SendToServer() 13 | elseif action == "boolean_right_pickup" then 14 | net.Start("VRButtonPresserMessage") 15 | net.WriteBool(false) -- Right hand 16 | net.SendToServer() 17 | end 18 | end 19 | end) 20 | end 21 | 22 | -- SERVER: When client reports a press, check for nearby buttons and press them 23 | if SERVER then 24 | local validClasses = { 25 | ["func_button"] = true, 26 | ["func_rot_button"] = true, 27 | ["item_healthcharger"] = true, 28 | ["item_suitcharger"] = true, 29 | ["item_ammo_crate"] = true, 30 | ["func_door_rotating"] = true, 31 | ["gmod_button"] = true, 32 | ["gmod_wire_button"] = true, 33 | ["sent_button"] = true 34 | } 35 | 36 | net.Receive("VRButtonPresserMessage", function(_, ply) 37 | if not ply:Alive() then return end 38 | local isLeftHand = net.ReadBool() 39 | local handPos = isLeftHand and vrmod.GetLeftHandPos(ply) or vrmod.GetRightHandPos(ply) 40 | if not handPos then return end 41 | local nearby = ents.FindInSphere(handPos, 5) 42 | for _, ent in ipairs(nearby) do 43 | if validClasses[ent:GetClass()] then 44 | ent:Use(ply) 45 | break 46 | end 47 | end 48 | end) 49 | end -------------------------------------------------------------------------------- /lua/vrmod/ui/cl_dermapopups.lua: -------------------------------------------------------------------------------- 1 | if SERVER then return end 2 | 3 | local meta = getmetatable(vgui.GetWorldPanel()) 4 | local orig = meta.MakePopup 5 | 6 | local allPopups = {} 7 | 8 | -- Generate a clean, unique identifier based on panel name 9 | local function getPanelIdentifier(panel) 10 | if not IsValid(panel) then return "popup_unknown" end 11 | local name = panel:GetName() or "Panel" 12 | name = name:lower():gsub("[^%w]", "") -- Sanitize: remove non-alphanumeric 13 | return "popup_" .. name 14 | end 15 | 16 | -- Overwrite MakePopup 17 | meta.MakePopup = function(...) 18 | local args = {...} 19 | orig(unpack(args)) 20 | if not g_VR.threePoints then return end 21 | 22 | local panel = args[1] 23 | if not IsValid(panel) then return end 24 | 25 | local uid = getPanelIdentifier(panel) 26 | allPopups[uid] = panel -- Overwrite any existing entry with same name 27 | 28 | timer.Simple(0.1, function() 29 | if not IsValid(panel) then return end 30 | panel:SetPaintedManually(true) 31 | 32 | local name = panel:GetName() 33 | if name == "DMenu" or name == "DImage" or name == "DPanel" then 34 | local child = panel:GetChildren()[1] 35 | if IsValid(child) then 36 | panel = child 37 | panel.Paint = function(self, w, h) 38 | surface.SetDrawColor(175, 174, 187) 39 | surface.DrawRect(0, 0, w, h) 40 | end 41 | end 42 | end 43 | 44 | local panelWidth, panelHeight = ScrW(), ScrH() 45 | VRUtilMenuOpen(uid, panelWidth, panelHeight, panel, true, Vector(10, 10, 5), Angle(0, -90, 50), 0.03, true, function() 46 | timer.Simple(0.1, function() 47 | if not g_VR.active and IsValid(panel) then 48 | panel:MakePopup() 49 | panel:RequestFocus() 50 | end 51 | end) 52 | -- Cleanup 53 | allPopups[uid] = nil 54 | end) 55 | 56 | VRUtilMenuRenderPanel(uid) 57 | end) 58 | end 59 | 60 | -- Continuously render active popups 61 | hook.Add("Think", "update_all_popups", function() 62 | for uid, panel in pairs(allPopups) do 63 | if IsValid(panel) then 64 | VRUtilMenuRenderPanel(uid) 65 | else 66 | allPopups[uid] = nil -- Auto-cleanup invalid panels 67 | end 68 | end 69 | end) 70 | -------------------------------------------------------------------------------- /lua/vrmod/input/cl_seated.lua: -------------------------------------------------------------------------------- 1 | if SERVER then return end 2 | local _, convarValues = vrmod.GetConvars() 3 | local seatedOffset, crouchOffset = Vector(), Vector() 4 | local function updateOffsetHook() 5 | seatedOffset.z = convarValues.vrmod_seated and convarValues.vrmod_seatedoffset or 0 6 | if seatedOffset.z == 0 and crouchOffset.z == 0 then 7 | hook.Remove("VRMod_Tracking", "seatedmode") 8 | return 9 | end 10 | 11 | hook.Add("VRMod_Tracking", "seatedmode", function() 12 | g_VR.tracking.hmd.pos = g_VR.tracking.hmd.pos + seatedOffset + crouchOffset 13 | g_VR.tracking.pose_lefthand.pos = g_VR.tracking.pose_lefthand.pos + seatedOffset + crouchOffset 14 | g_VR.tracking.pose_righthand.pos = g_VR.tracking.pose_righthand.pos + seatedOffset + crouchOffset 15 | end) 16 | end 17 | 18 | vrmod.AddCallbackedConvar("vrmod_seatedoffset", nil, "0", nil, nil, nil, nil, tonumber, function(val) updateOffsetHook() end) 19 | vrmod.AddCallbackedConvar("vrmod_seated", nil, "0", nil, nil, nil, nil, tobool, function(val) updateOffsetHook() end) 20 | 21 | 22 | hook.Add("VRMod_Start", "seatedmode", function(ply) 23 | if ply ~= LocalPlayer() then return end 24 | updateOffsetHook() 25 | end) 26 | 27 | local crouchTarget = 0 28 | hook.Add("VRMod_Input", "crouching", function(action, pressed) 29 | if action == "boolean_crouch" and pressed then 30 | crouchTarget = crouchTarget == 0 and math.min(0, 38 - (g_VR.tracking.hmd.pos.z - g_VR.origin.z)) or 0 --vrmod default crouch threshold is 40 31 | local speed = (crouchTarget == 0 and 36 or -36) * 1 / LocalPlayer():GetDuckSpeed() --eye pos difference between standing and crouched gmod player is 36 units, this distance is travelled in GetDuckSpeed seconds 32 | hook.Add("PreRender", "vrmod_crouch", function() 33 | crouchOffset.z = crouchOffset.z + speed * FrameTime() 34 | if crouchOffset.z > 0 or crouchTarget < 0 and crouchOffset.z < crouchTarget then 35 | crouchOffset.z = crouchTarget 36 | hook.Remove("PreRender", "vrmod_crouch") 37 | updateOffsetHook() 38 | end 39 | end) 40 | 41 | crouchOffset.z = crouchOffset.z + 0.01 42 | updateOffsetHook() 43 | end 44 | end) -------------------------------------------------------------------------------- /lua/vrmod/utils/sh_weps.lua: -------------------------------------------------------------------------------- 1 | g_VR = g_VR or {} 2 | vrmod = vrmod or {} 3 | vrmod.utils = vrmod.utils or {} 4 | vrmod.suppressViewModelUpdates = false 5 | -- WEP UTILS 6 | function vrmod.utils.IsValidWep(wep, get) 7 | if not IsValid(wep) then return false end 8 | local class = wep:GetClass() 9 | local vm 10 | vm = wep:GetWeaponViewModel() 11 | if class == "weapon_vrmod_empty" or vm == "" or vm == "models/weapons/c_arms.mdl" then return false end 12 | if get then 13 | return class, vm 14 | else 15 | return true 16 | end 17 | end 18 | 19 | function vrmod.utils.IsWeaponEntity(ent) 20 | if not IsValid(ent) then return false end 21 | local c = ent:GetClass() 22 | return ent:IsWeapon() or c:find("weapon_") or c == "prop_physics" and ent:GetModel():find("w_") 23 | end 24 | 25 | function vrmod.utils.WepInfo(wep) 26 | local class, vm = vrmod.utils.IsValidWep(wep, true) 27 | if class and vm then return class, vm end 28 | end 29 | 30 | function vrmod.utils.UpdateViewModelPos(pos, ang, override) 31 | local ply = LocalPlayer() 32 | if vrmod.suppressViewModelUpdates and not override then 33 | vrmod.utils.UpdateViewModel() 34 | return 35 | end 36 | 37 | pos, ang = vrmod.utils.CheckWeaponPushout(pos, ang) 38 | if not IsValid(ply) or not g_VR.active then return end 39 | if not ply:Alive() then return end 40 | local currentvmi = g_VR.currentvmi 41 | local modelPos = pos 42 | if currentvmi then 43 | local collisionShape = vrmod._collisionShapeByHand and vrmod._collisionShapeByHand.right 44 | if collisionShape and collisionShape.isClipped and collisionShape.pushOutPos then 45 | modelPos = collisionShape.pushOutPos 46 | vrmod.logger.Debug("[VRMod] Applying collision-corrected pos for viewmodel:", modelPos) 47 | end 48 | 49 | local offsetPos, offsetAng = LocalToWorld(currentvmi.offsetPos, currentvmi.offsetAng, modelPos, ang) 50 | g_VR.viewModelPos = offsetPos 51 | g_VR.viewModelAng = offsetAng 52 | vrmod.utils.UpdateViewModel() 53 | end 54 | end 55 | 56 | function vrmod.utils.UpdateViewModel() 57 | local vm = g_VR.viewModel 58 | if IsValid(vm) then 59 | if not g_VR.usingWorldModels then 60 | vm:SetPos(g_VR.viewModelPos) 61 | vm:SetAngles(g_VR.viewModelAng) 62 | vm:SetupBones() 63 | end 64 | 65 | g_VR.viewModelMuzzle = vm:GetAttachment(1) 66 | end 67 | end -------------------------------------------------------------------------------- /lua/vrmod/api/sh_api.lua: -------------------------------------------------------------------------------- 1 | local addonVersion = 200 2 | if system.IsLinux() then 3 | requiredModuleVersion = 23 4 | else 5 | requiredModuleVersion = 21 6 | end 7 | 8 | g_VR = g_VR or {} 9 | vrmod = vrmod or {} 10 | local convars, convarValues = {}, {} 11 | function vrmod.AddCallbackedConvar(cvarName, valueName, defaultValue, flags, helptext, min, max, conversionFunc, callbackFunc) 12 | valueName = valueName or cvarName 13 | flags = flags or FCVAR_ARCHIVE 14 | conversionFunc = conversionFunc or function(val) return val end 15 | -- Prevent re-creating existing convar 16 | local cv = GetConVar(cvarName) 17 | if not cv then cv = CreateConVar(cvarName, defaultValue, flags, helptext, min, max) end 18 | convars[cvarName] = cv 19 | convarValues[valueName] = conversionFunc(cv:GetString()) 20 | -- Set up dynamic callback 21 | cvars.AddChangeCallback(cvarName, function(_, _, new) 22 | convarValues[valueName] = conversionFunc(new) 23 | if callbackFunc then callbackFunc(convarValues[valueName]) end 24 | end, "vrmod") 25 | return convars, convarValues 26 | end 27 | 28 | function vrmod.GetConvars() 29 | return convars, convarValues 30 | end 31 | 32 | function vrmod.GetVersion() 33 | return addonVersion 34 | end 35 | 36 | hook.Add("PlayerDisconnected", "VRMod_CleanCache", function(ply) vrmod.HandVelocityCache[ply:SteamID()] = nil end) 37 | local hookTranslations = { 38 | VRUtilEventTracking = "VRMod_Tracking", 39 | VRUtilEventInput = "VRMod_Input", 40 | VRUtilEventPreRender = "VRMod_PreRender", 41 | VRUtilEventPreRenderRight = "VRMod_PreRenderRight", 42 | VRUtilEventPostRender = "VRMod_PostRender", 43 | VRUtilStart = "VRMod_Start", 44 | VRUtilExit = "VRMod_Exit", 45 | VRUtilEventPickup = "VRMod_Pickup", 46 | VRUtilEventDrop = "VRMod_Drop", 47 | VRUtilAllowDefaultAction = "VRMod_AllowDefaultAction" 48 | } 49 | 50 | local hooks = hook.GetTable() 51 | for k, v in pairs(hooks) do 52 | local translation = hookTranslations[k] 53 | if translation then 54 | hooks[translation] = hooks[translation] or {} 55 | for k2, v2 in pairs(v) do 56 | hooks[translation][k2] = v2 57 | end 58 | 59 | hooks[k] = nil 60 | end 61 | end 62 | 63 | local orig = hook.Add 64 | hook.Add = function(...) 65 | local args = {...} 66 | args[1] = hookTranslations[args[1]] or args[1] 67 | orig(unpack(args)) 68 | end 69 | 70 | local orig = hook.Remove 71 | hook.Remove = function(...) 72 | local args = {...} 73 | args[1] = hookTranslations[args[1]] or args[1] 74 | orig(unpack(args)) 75 | end -------------------------------------------------------------------------------- /lua/vrmod/player/sh_flashlight.lua: -------------------------------------------------------------------------------- 1 | if CLIENT then 2 | local convars, convarValues = vrmod.AddCallbackedConvar("vrmod_flashlight_attachment", nil, "0", nil, nil, 0, 2, function(val) return math.floor(tonumber(val) or 0) end) 3 | local attachments = {"pose_righthand", "pose_lefthand", "hmd",} 4 | local flashlight = nil 5 | net.Receive("vrmod_flashlight", function() 6 | local enabled = net.ReadBool() 7 | if not flashlight and enabled then 8 | surface.PlaySound("items/flashlight1.wav") 9 | flashlight = ProjectedTexture() 10 | flashlight:SetTexture("effects/flashlight001") 11 | flashlight:SetFOV(GetConVar("r_flashlightfov"):GetFloat()) 12 | flashlight:SetFarZ(GetConVar("r_flashlightfar"):GetFloat()) 13 | hook.Add("VRMod_PreRender", "flashlight", function() 14 | if not g_VR.threePoints then return end 15 | local pos = g_VR.tracking[attachments[convarValues.vrmod_flashlight_attachment + 1]].pos 16 | local ang = g_VR.tracking[attachments[convarValues.vrmod_flashlight_attachment + 1]].ang 17 | if convarValues.vrmod_flashlight_attachment == 0 and g_VR.viewModelMuzzle then 18 | pos = g_VR.viewModelMuzzle.Pos 19 | if not (g_VR.currentvmi and g_VR.currentvmi.wrongMuzzleAng) then ang = g_VR.viewModelMuzzle.Ang end 20 | end 21 | 22 | flashlight:SetPos(pos + ang:Forward() * 10) 23 | flashlight:SetAngles(ang) 24 | flashlight:Update() 25 | end) 26 | elseif flashlight then 27 | surface.PlaySound("items/flashlight1.wav") 28 | hook.Remove("VRMod_PreRender", "flashlight") 29 | flashlight:Remove() 30 | flashlight = nil 31 | end 32 | end) 33 | 34 | hook.Add("VRMod_Exit", "flashlight", function(ply, steamid) 35 | if ply == LocalPlayer() and flashlight then 36 | hook.Remove("VRMod_PreRender", "flashlight") 37 | flashlight:Remove() 38 | flashlight = nil 39 | end 40 | end) 41 | elseif SERVER then 42 | util.AddNetworkString("vrmod_flashlight") 43 | local skip = false 44 | hook.Add("PlayerSwitchFlashlight", "vrmod_flashlight", function(ply, enabled) 45 | if skip then return end 46 | if g_VR[ply:SteamID()] then 47 | skip = true 48 | local res = hook.Run("PlayerSwitchFlashlight", ply, enabled) 49 | skip = false 50 | if res == false then return end 51 | net.Start("vrmod_flashlight") 52 | net.WriteBool(ply.m_bFlashlight ~= false and enabled) 53 | net.Send(ply) 54 | if enabled then 55 | return false --don't turn on the default flashlight cus we're using a custom one for vr 56 | end 57 | end 58 | end) 59 | end -------------------------------------------------------------------------------- /lua/vrmod/loader.lua: -------------------------------------------------------------------------------- 1 | function VRMod_Loader(folder) 2 | -- Ensure folder ends with a slash (pure Lua, no EndWith) 3 | if folder:sub(-1) ~= "/" then folder = folder .. "/" end 4 | -- Determine subsystem name (last folder) 5 | local parts = string.Explode("/", string.TrimRight(folder, "/")) 6 | local subsystemName = parts[#parts] or folder 7 | -- Helper function to include files 8 | local function includeFile(f) 9 | local path = folder .. f 10 | if f:StartWith("sh_") then 11 | if SERVER then AddCSLuaFile(path) end 12 | include(path) 13 | --MsgC(Color(150, 0, 200), "[VRMod] [SH] ", color_white, f, "\n") 14 | return 15 | end 16 | 17 | if f:StartWith("sv_") then 18 | if SERVER then 19 | include(path) 20 | --MsgC(Color(200, 150, 0), "[VRMod] [SV] ", color_white, f, "\n") 21 | end 22 | return 23 | end 24 | 25 | if f:StartWith("cl_") then 26 | if SERVER then 27 | AddCSLuaFile(path) 28 | --MsgC(Color(0, 180, 255), "[VRMod] [->CL] ", color_white, f, "\n") 29 | else 30 | include(path) 31 | --MsgC(Color(0, 180, 255), "[VRMod] [CL] ", color_white, f, "\n") 32 | end 33 | return 34 | end 35 | end 36 | 37 | -- Scan files in folder 38 | local files, _ = file.Find(folder .. "*.lua", "LUA") 39 | table.sort(files) 40 | -- Separate by prefix to guarantee load order 41 | local sh_files, sv_files, cl_files = {}, {}, {} 42 | for _, f in ipairs(files) do 43 | if f ~= "loader.lua" and f ~= "init.lua" then 44 | if f:StartWith("sh_") then 45 | table.insert(sh_files, f) 46 | elseif f:StartWith("sv_") then 47 | table.insert(sv_files, f) 48 | elseif f:StartWith("cl_") then 49 | table.insert(cl_files, f) 50 | end 51 | end 52 | end 53 | 54 | -- Include files in order: shared -> server -> client 55 | for _, f in ipairs(sh_files) do 56 | includeFile(f) 57 | end 58 | 59 | for _, f in ipairs(sv_files) do 60 | includeFile(f) 61 | end 62 | 63 | for _, f in ipairs(cl_files) do 64 | includeFile(f) 65 | end 66 | 67 | -- Mark subsystem as loaded 68 | vrmod = vrmod or {} 69 | vrmod.status = vrmod.status or {} 70 | vrmod.status[subsystemName] = true 71 | -- Final log 72 | MsgC(Color(0, 200, 0), "[VRMod] Subsystem initialized: ", color_white, string.upper(subsystemName), "\n") 73 | end -------------------------------------------------------------------------------- /lua/vrmod/ui/cl_quickmenu.lua: -------------------------------------------------------------------------------- 1 | if SERVER then return end 2 | local open = false 3 | function g_VR.MenuOpen() 4 | if hook.Call("VRMod_OpenQuickMenu") == false then return end 5 | if open then return end 6 | open = true 7 | -- 8 | local items = {} 9 | for k, v in pairs(g_VR.menuItems) do 10 | local slot, slotPos = v.slot, v.slotPos 11 | local index = #items + 1 12 | for i = 1, #items do 13 | if items[i].slot > slot or items[i].slot == slot and items[i].slotPos > slotPos then 14 | index = i 15 | break 16 | end 17 | end 18 | 19 | table.insert(items, index, { 20 | index = k, 21 | slot = slot, 22 | slotPos = slotPos 23 | }) 24 | end 25 | 26 | local currentSlot, actualSlotPos = 0, 0 27 | for i = 1, #items do 28 | if items[i].slot ~= currentSlot then 29 | actualSlotPos = 0 30 | currentSlot = items[i].slot 31 | end 32 | 33 | items[i].actualSlotPos = actualSlotPos 34 | actualSlotPos = actualSlotPos + 1 35 | end 36 | 37 | -- 38 | local prevHoveredItem = -2 39 | VRUtilMenuOpen("miscmenu", 512, 512, nil, true, Vector(4, 3, 9.5), Angle(0, -90, 60), 0.03, true, function() 40 | hook.Remove("PreRender", "vrutil_hook_renderigm") 41 | open = false 42 | if items[prevHoveredItem] and g_VR.menuItems[items[prevHoveredItem].index] then g_VR.menuItems[items[prevHoveredItem].index].func() end 43 | end) 44 | 45 | hook.Add("PreRender", "vrutil_hook_renderigm", function() 46 | hoveredItem = -1 47 | local hoveredSlot, hoveredSlotPos = -1, -1 48 | if g_VR.menuFocus == "miscmenu" then hoveredSlot, hoveredSlotPos = math.floor(g_VR.menuCursorX / 86), math.floor((g_VR.menuCursorY - 230) / 57) end 49 | for i = 1, #items do 50 | if items[i].slot == hoveredSlot and items[i].actualSlotPos == hoveredSlotPos then 51 | hoveredItem = i 52 | break 53 | end 54 | end 55 | 56 | local changes = hoveredItem ~= prevHoveredItem 57 | prevHoveredItem = hoveredItem 58 | if not changes then return end 59 | VRUtilMenuRenderStart("miscmenu") 60 | --buttons 61 | local buttonWidth, buttonHeight = 82, 53 62 | local gap = (512 - buttonWidth * 6) / 5 63 | for i = 1, #items do 64 | local x, y = items[i].slot, items[i].actualSlotPos 65 | draw.RoundedBox(8, x * (buttonWidth + gap), 230 + y * (buttonHeight + gap), buttonWidth, buttonHeight, Color(0, 0, 0, hoveredItem == i and 200 or 128)) 66 | local explosion = string.Explode(" ", g_VR.menuItems[items[i].index].name, false) 67 | for j = 1, #explosion do 68 | draw.SimpleText(explosion[j], "HudSelectionText", buttonWidth / 2 + x * (buttonWidth + gap), 230 + buttonHeight / 2 + y * (buttonHeight + gap) - (#explosion * 6 - 6 - (j - 1) * 12), Color(255, 255, 255, 255), TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER) 69 | end 70 | end 71 | 72 | VRUtilMenuRenderEnd() 73 | end) 74 | end 75 | 76 | function g_VR.MenuClose() 77 | VRUtilMenuClose("miscmenu") 78 | end -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## **GarrysMod VRMod x64 – Extended** 2 | 3 | ![VRmodX64-JayPegged](https://github.com/user-attachments/assets/d08e80cd-b8e6-4348-b56d-70e2e79fa8a6) 4 | 5 | 6 | ### ⚠️ Optimization Issues 7 | 8 | VRMod and its components—such as hand physics, melee attacks, and item interaction—are maintained by different authors. This often results in compatibility issues, broken features, or abandoned modules. 9 | 10 | This build focuses on **optimization** by merging essential features from semi-official forks and third-party addons, with an emphasis on performance, cross-platform stability, and code de-duplication. 11 | 12 | --- 13 | 14 | ### ✅ Key Features 15 | 16 | - Refactored codebase for improved stability and cross-platform compatibility 17 | - Fixed rendering issues on Linux (native x64 builds) 18 | - Fully supported on Windows (both x64 and Legacy branches) 19 | - Improved UI with new rendering settings 20 | - Cursor stability fixed in spawn menu and popups 21 | - Better performance and reduced latency across systems 22 | - Integrated hand collision physics for props (no more unintended prop sounds) 23 | - Added clientside wall collisions for hands and SWEPs 24 | - Rewritten pickup system: 25 | - Manual item pickup 26 | - Multiplayer-friendly design 27 | - Adds halos for visual clarity 28 | - Serverside weight limit 29 | - Clientside precalculation to reduce server load 30 | - Supports picking up NPCs 31 | - Interactive world buttons 32 | - Keypad tool support 33 | - Support for dropping and picking up non-VR weapons 34 | - Melee system overhauled: trace-based with velocity-scaled damage + bonus for weapon impact 35 | - Functional numpad input in VR 36 | - Glide support 37 | - Motion driving with wheel gripping (engine based vehicles + Glide) Don't forget to bind pickups for grip buttons 38 | - Shooting while driving. (ArcVR works for all vehicles, standard SWEPs work only if collisions allow it, like jalopy or glide motorbikes and some roofless cars) Need to bind "weaponmenu", "reload", "turret" for primary and "alt_turret" for secondary fire in vehicle tab 39 | - Motion-controlled physgun: rotation and movement based on hand motion 40 | - Gravity gun now supports prop rotation, just like HL2 VR 41 | - UI now works correctly while in vehicles (given the mouse click is set in bindings for vehicle) 42 | - Likely more small fixes and improvements under the hood 43 | 44 | 45 | ### 📦 Installation 46 | 47 | **Requirements:** 48 | 49 | - Ensure your system supports **GMod x64**. 50 | - On native Linux, run the following script first:[GModCEFCodecFix](https://github.com/solsticegamestudios/GModCEFCodecFix) 51 | - For trully native experience, use [Steam-Play-None](https://github.com/Scrumplex/Steam-Play-None) 52 | - Please note that only ALVR is now supported. 53 | 54 | **Installation:** 55 | 56 | 1. Download the latest precompiled modules: [Releases Page](https://github.com/Abyss-c0re/vrmod-module-master/releases) 57 | 2. Subscribe to the Workshop addon: 58 | [Steam Workshop – VRMod](https://steamcommunity.com/sharedfiles/filedetails/?id=3442302711) 59 | 60 | **OR** 61 | 62 | Clone or download this repository manually: 63 | 64 | - Rename the folder to `vrmod` (do **not** use dashes `-`) 65 | - Place it in: 66 | `./GarrysMod/garrysmod/addons/vrmod` 67 | 68 | --- 69 | -------------------------------------------------------------------------------- /lua/vrmod/pickup/sh_manualpickup.lua: -------------------------------------------------------------------------------- 1 | local vrmod_manualpickup = CreateConVar("vrmod_manualpickups", 1, {FCVAR_ARCHIVE, FCVAR_REPLICATED}, "vrmod manual pickup toggle") 2 | -- Per-player states 3 | local PickupDisabled = {} 4 | local PickupDisabledWeapons = {} 5 | -- Initialize pickup states on spawn 6 | hook.Add("PlayerSpawn", "SpawnSetPickupState", function(ply) 7 | local id = ply:EntIndex() 8 | PickupDisabled[id] = true 9 | PickupDisabledWeapons[id] = true 10 | end) 11 | 12 | -- Set player as VR when entering VRMod 13 | hook.Add("VRMod_Start", "VRModPickupStartState", function(ply) 14 | ply:SetNWBool("IsVR", true) 15 | local id = ply:EntIndex() 16 | PickupDisabled[id] = true 17 | PickupDisabledWeapons[id] = true 18 | end) 19 | 20 | -- Clear VR state when exiting VRMod 21 | hook.Add("VRMod_Exit", "VRModPickupResetState", function(ply) 22 | ply:SetNWBool("IsVR", false) 23 | local id = ply:EntIndex() 24 | PickupDisabled[id] = nil 25 | PickupDisabledWeapons[id] = nil 26 | end) 27 | 28 | -- Fix VR state loss after respawn 29 | timer.Create("VRModManualPickup_RespawnFixTimer", 1, 0, function() 30 | for _, ply in ipairs(player.GetAll()) do 31 | if ply:Alive() and ply:GetNWBool("IsVR", false) then 32 | local id = ply:EntIndex() 33 | if PickupDisabled[id] == nil then 34 | PickupDisabled[id] = true 35 | PickupDisabledWeapons[id] = true 36 | end 37 | end 38 | end 39 | end) 40 | 41 | -- Handle item drop to allow manual pickup 42 | hook.Add("VRMod_Drop", "ManualItemPickupDropHook", function(ply, ent) 43 | if not IsValid(ent) then return end 44 | if ent:GetClass() == "prop_physics" then return end 45 | local id = ply:EntIndex() 46 | PickupDisabled[id] = false 47 | timer.Simple(0.3, function() if IsValid(ply) then PickupDisabled[id] = true end end) 48 | end) 49 | 50 | -- Handle weapon pickup by hand 51 | hook.Add("VRMod_Pickup", "ManualWeaponPickupHook", function(ply, ent) 52 | if not IsValid(ply) or not ply:IsPlayer() then return end 53 | if not IsValid(ent) or not ent:IsWeapon() then return end 54 | if not ply.PickupWeapon then return end 55 | local wepClass = ent:GetClass() 56 | -- Temporarily disable pickup protection 57 | hook.Call("VRMod_Drop", nil, ply, ent) 58 | -- Try to replace with VR weapon 59 | local replacement = vrmod.utils.ReplaceWeapon(ply, ent) 60 | if replacement then wepClass = replacement end 61 | if ply:HasWeapon(wepClass) then 62 | -- Give ammo based on dropped weapon's clip 63 | local ammoType = ent:GetPrimaryAmmoType() 64 | local clip = ent:Clip1() 65 | if ammoType >= 0 and clip > 0 then ply:GiveAmmo(clip, ammoType, true) end 66 | ply:SelectWeapon(wepClass) 67 | else 68 | ply:Give(wepClass, true) 69 | timer.Simple(0, function() if IsValid(ply) then ply:SelectWeapon(wepClass) end end) 70 | ent:Remove() 71 | end 72 | end) 73 | 74 | -- Disable touch-based item pickup 75 | hook.Add("PlayerCanPickupItem", "ItemTouchPickupDisablerVR", function(ply, item) 76 | local id = ply:EntIndex() 77 | if vrmod_manualpickup:GetBool() and item:GetClass() ~= "item_suit" and PickupDisabled[id] and ply:GetNWBool("IsVR", false) then return false end 78 | return true 79 | end) 80 | 81 | -- Disable touch-based weapon pickup 82 | hook.Add("PlayerCanPickupWeapon", "WeaponTouchPickupDisablerVR", function(ply, wep) 83 | local id = ply:EntIndex() 84 | if vrmod_manualpickup:GetBool() and wep:GetPos() ~= ply:GetPos() and PickupDisabledWeapons[id] and ply:GetNWBool("IsVR", false) then return false end 85 | end) -------------------------------------------------------------------------------- /lua/vrmod/input/sh_gravity_and_physgun.lua: -------------------------------------------------------------------------------- 1 | if CLIENT then 2 | local lastHandPos = nil 3 | local lastHandAng = nil 4 | local function VRPhysgunControl(cmd) 5 | local hand = g_VR.tracking.pose_lefthand 6 | if not hand then return end 7 | local newPos = hand.pos 8 | local newAng = hand.ang 9 | local deltaPos = newPos - lastHandPos 10 | local deltaAng = Angle(math.AngleDifference(newAng.pitch, lastHandAng.pitch), math.AngleDifference(newAng.yaw, lastHandAng.yaw), math.AngleDifference(newAng.roll, lastHandAng.roll)) 11 | -- Forward/backward motion detection 12 | local forward = EyeAngles():Forward() 13 | local forwardDelta = forward:Dot(deltaPos) * 10 14 | if forwardDelta > 0.3 then 15 | cmd:SetButtons(bit.bor(cmd:GetButtons(), IN_FORWARD)) 16 | elseif forwardDelta < -0.3 then 17 | cmd:SetButtons(bit.bor(cmd:GetButtons(), IN_BACK)) 18 | end 19 | 20 | -- Mouse movement from hand rotation 21 | cmd:SetMouseX(deltaAng.yaw * 50) 22 | cmd:SetMouseY(-deltaAng.pitch * 50) 23 | -- Update for next frame 24 | lastHandPos = newPos 25 | lastHandAng = newAng 26 | end 27 | 28 | hook.Add("VRMod_Input", "physgun_controll", function(action, pressed) 29 | if hook.Call("VRMod_AllowDefaultAction", nil, action) == false then return end 30 | if action == "boolean_use" or action == "boolean_exit" then 31 | if pressed then 32 | LocalPlayer():ConCommand("+use") 33 | local wep = LocalPlayer():GetActiveWeapon() 34 | if IsValid(wep) and wep:GetClass() == "weapon_physgun" then 35 | lastHandPos = g_VR.tracking.pose_lefthand.pos 36 | lastHandAng = g_VR.tracking.pose_lefthand.ang 37 | hook.Add("CreateMove", "vrutil_hook_cmphysguncontrol", VRPhysgunControl) 38 | end 39 | else 40 | LocalPlayer():ConCommand("-use") 41 | hook.Remove("CreateMove", "vrutil_hook_cmphysguncontrol") 42 | end 43 | return 44 | end 45 | end) 46 | elseif SERVER then 47 | hook.Add("GravGunOnPickedUp", "VR_TrackHeldEntity", function(ply, ent) 48 | if not vrmod.IsPlayerInVR(ply) then return end 49 | ply.VRHeldEnt = ent 50 | ply.VRTargetAngles = ent:GetAngles() 51 | hook.Add("Think", "VR_ApplyRotation_" .. ply:SteamID(), function() 52 | if not IsValid(ply) or not IsValid(ply.VRHeldEnt) then 53 | hook.Remove("Think", "VR_ApplyRotation_" .. ply:SteamID()) 54 | return 55 | end 56 | 57 | local phys = ply.VRHeldEnt:GetPhysicsObject() 58 | local targetAngles = vrmod.GetRightHandAng(ply) 59 | ply.VRTargetAngles = LerpAngle(0.15, ply.VRTargetAngles or targetAngles, targetAngles) 60 | if IsValid(phys) then 61 | phys:SetAngleVelocityInstantaneous(Vector(0, 0, 0)) 62 | phys:SetAngles(ply.VRTargetAngles) 63 | else 64 | ply.VRHeldEnt:SetAngles(ply.VRTargetAngles) 65 | end 66 | end) 67 | end) 68 | 69 | hook.Add("GravGunOnDropped", "VR_ClearHeldEntity", function(ply, ent) 70 | if ply.VRHeldEnt == ent then 71 | ply.VRHeldEnt = nil 72 | ply.VRTargetAngles = nil 73 | hook.Remove("Think", "VR_ApplyRotation_" .. ply:SteamID()) 74 | end 75 | end) 76 | end -------------------------------------------------------------------------------- /lua/vrmod/ui/cl_buttons.lua: -------------------------------------------------------------------------------- 1 | if SERVER then return end 2 | local function InitializeMenuItems() 3 | g_VR.menuItems = {} 4 | -- Row 1 5 | vrmod.AddInGameMenuItem("Spawn Menu", 0, 0, function() 6 | if not IsValid(g_SpawnMenu) then return end 7 | g_SpawnMenu:Open() 8 | hook.Add("VRMod_OpenQuickMenu", "close_spawnmenu", function() 9 | hook.Remove("VRMod_OpenQuickMenu", "close_spawnmenu") 10 | g_SpawnMenu:Close() 11 | return false 12 | end) 13 | end) 14 | 15 | vrmod.AddInGameMenuItem("Context Menu", 1, 0, function() 16 | if not IsValid(g_ContextMenu) then return end 17 | g_ContextMenu:Open() 18 | hook.Add("VRMod_OpenQuickMenu", "closecontextmenu", function() 19 | hook.Remove("VRMod_OpenQuickMenu", "closecontextmenu") 20 | g_ContextMenu:Close() 21 | return false 22 | end) 23 | end) 24 | 25 | vrmod.AddInGameMenuItem("Chat", 2, 0, function() LocalPlayer():ConCommand("vrmod_chatmode") end) 26 | vrmod.AddInGameMenuItem("Numpad", 3, 0, function() LocalPlayer():ConCommand("vrmod_numpad") end) 27 | vrmod.AddInGameMenuItem("Mirror", 4, 0, function() VRUtilOpenHeightMenu() end) 28 | vrmod.AddInGameMenuItem("Settings", 5, 0, function() 29 | local frame = VRUtilOpenMenu() 30 | hook.Add("VRMod_OpenQuickMenu", "closesettings", function() 31 | hook.Remove("VRMod_OpenQuickMenu", "closesettings") 32 | if IsValid(frame) then 33 | frame:Remove() 34 | return false 35 | end 36 | end) 37 | end) 38 | 39 | -- Row 2 40 | vrmod.AddInGameMenuItem("Flashlight", 0, 1, function() LocalPlayer():ConCommand("impulse 100") end) 41 | vrmod.AddInGameMenuItem("Laser pointer", 1, 1, function() LocalPlayer():ConCommand("vrmod_togglelaserpointer") end) 42 | vrmod.AddInGameMenuItem("Toggle Noclip", 2, 1, function() LocalPlayer():ConCommand("noclip") end) 43 | vrmod.AddInGameMenuItem("Undo", 3, 1, function() LocalPlayer():ConCommand("gmod_undo") end) 44 | vrmod.AddInGameMenuItem("Cleanup", 4, 1, function() LocalPlayer():ConCommand("gmod_cleanup") end) 45 | vrmod.AddInGameMenuItem("Admin Cleanup", 5, 1, function() LocalPlayer():ConCommand("gmod_admin_cleanup") end) 46 | -- Row 3 47 | vrmod.AddInGameMenuItem("Reset Vehicle View", 0, 2, function() VRUtilresetVehicleView() end) 48 | vrmod.AddInGameMenuItem("UI Reset", 1, 2, function() LocalPlayer():ConCommand("vrmod_vgui_reset") end) 49 | vrmod.AddInGameMenuItem("Toggle blacklist weapon", 2, 2, function() LocalPlayer():ConCommand("vrmod_toggle_blacklist") end) 50 | vrmod.AddInGameMenuItem("Map Browser", 3, 2, function() 51 | local window = VRUtilCreateMapBrowserWindow() 52 | hook.Add("VRMod_OpenQuickMenu", "closemapbrowser", function() 53 | hook.Remove("VRMod_OpenQuickMenu", "closemapbrowser") 54 | if IsValid(window) then window:Remove() end 55 | return false 56 | end) 57 | end) 58 | 59 | vrmod.AddInGameMenuItem("RESPAWN", 4, 2, function() LocalPlayer():ConCommand("kill") end) 60 | vrmod.AddInGameMenuItem("VR EXIT", 5, 2, function() LocalPlayer():ConCommand("vrmod_exit") end) 61 | --vrmod.AddInGameMenuItem("DISCONNECT", 5, 2, function() LocalPlayer():ConCommand("disconnect") end) 62 | -- Row 4 63 | -- Exit VR hook to fix spawnmenu after exiting VR 64 | end 65 | 66 | hook.Add("VRMod_Start", "ReloadMenuItems", function() InitializeMenuItems() end) 67 | hook.Add("VRMod_Exit", "restore_spawnmenu", function(ply) 68 | if ply ~= LocalPlayer() then return end 69 | timer.Simple(0.1, function() if IsValid(g_SpawnMenu) and g_SpawnMenu.HorizontalDivider ~= nil then g_SpawnMenu.HorizontalDivider:SetLeftWidth(ScrW()) end end) 70 | end) -------------------------------------------------------------------------------- /lua/vrmod/utils/cl_rendering.lua: -------------------------------------------------------------------------------- 1 | g_VR = g_VR or {} 2 | vrmod = vrmod or {} 3 | vrmod.utils = vrmod.utils or {} 4 | 5 | function vrmod.utils.CalculateProjectionParams(projMatrix, worldScale) 6 | local xscale = projMatrix[1][1] 7 | local xoffset = projMatrix[1][3] 8 | local yscale = projMatrix[2][2] 9 | local yoffset = projMatrix[2][3] 10 | -- ** Normalize vertical sign: ** 11 | if not system.IsWindows() then 12 | -- On Linux/OpenGL: invert the sign so + means “down” just like on Windows 13 | yoffset = -yoffset 14 | end 15 | 16 | -- now the rest is identical on both platforms: 17 | local tan_px = math.abs((1 - xoffset) / xscale) 18 | local tan_nx = math.abs((-1 - xoffset) / xscale) 19 | local tan_py = math.abs((1 - yoffset) / yscale) 20 | local tan_ny = math.abs((-1 - yoffset) / yscale) 21 | local w = (tan_px + tan_nx) / worldScale 22 | local h = (tan_py + tan_ny) / worldScale 23 | return { 24 | HorizontalFOV = math.deg(2 * math.atan(w / 2)), 25 | AspectRatio = w / h, 26 | HorizontalOffset = xoffset, 27 | VerticalOffset = yoffset, 28 | Width = w, 29 | Height = h, 30 | } 31 | end 32 | 33 | function vrmod.utils.ComputeSubmitBounds(leftCalc, rightCalc, hOffset, vOffset, scaleFactor, renderOffset) 34 | local isWindows = system.IsWindows() 35 | local hFactor, vFactor = 0, 0 36 | -- average half‐eye extents in tangent space 37 | if renderOffse then 38 | local wAvg = (leftCalc.Width + rightCalc.Width) * 0.5 39 | local hAvg = (leftCalc.Height + rightCalc.Height) * 0.5 40 | hFactor = 0.5 / wAvg 41 | vFactor = 1.0 / hAvg 42 | else 43 | --original calues 44 | hFactor = 0.25 45 | vFactor = 0.5 46 | end 47 | 48 | hFactor = hFactor * scaleFactor 49 | vFactor = vFactor * scaleFactor 50 | -- UV origin flip only affects V‐range endpoints, not the offset sign: 51 | local vMin, vMax = isWindows and 0 or 1, isWindows and 1 or 0 52 | local function calcVMinMax(offset) 53 | local adj = offset * vFactor 54 | return vMin - adj, vMax - adj 55 | end 56 | 57 | -- U bounds 58 | local uMinLeft = 0.0 + (leftCalc.HorizontalOffset + hOffset) * hFactor 59 | local uMaxLeft = 0.5 + (leftCalc.HorizontalOffset + hOffset) * hFactor 60 | local uMinRight = 0.5 + (rightCalc.HorizontalOffset + hOffset) * hFactor 61 | local uMaxRight = 1.0 + (rightCalc.HorizontalOffset + hOffset) * hFactor 62 | -- V bounds 63 | local vMinLeft, vMaxLeft = calcVMinMax(leftCalc.VerticalOffset + vOffset) 64 | local vMinRight, vMaxRight = calcVMinMax(rightCalc.VerticalOffset + vOffset) 65 | return uMinLeft, vMinLeft, uMaxLeft, vMaxLeft, uMinRight, vMinRight, uMaxRight, vMaxRight 66 | end 67 | 68 | function vrmod.utils.ComputeDesktopCrop(desktopView, w, h) 69 | local vmargin = (1 - ScrH() / ScrW() * w / 2 / h) / 2 70 | local hoffset = desktopView == 3 and 0.5 or 0 71 | return vmargin, hoffset 72 | end 73 | 74 | function vrmod.utils.AdjustFOV(proj, fovScaleX, fovScaleY) 75 | local clone = {} 76 | for i = 1, 4 do 77 | clone[i] = {proj[i][1], proj[i][2], proj[i][3], proj[i][4]} 78 | end 79 | 80 | -- scale the FOV (diagonal terms) 81 | clone[1][1] = clone[1][1] * fovScaleX 82 | clone[2][2] = clone[2][2] * fovScaleY 83 | -- scale the center offset (asymmetry) terms 84 | clone[1][3] = clone[1][3] * fovScaleX 85 | clone[2][3] = clone[2][3] * fovScaleY 86 | return clone 87 | end 88 | 89 | function vrmod.utils.DrawDeathAnimation(rtWidth, rtHeight) 90 | if not g_VR.deathTime then g_VR.deathTime = CurTime() end 91 | local fadeAlpha = 0 92 | local fadeDuration = 3.5 93 | local maxAlpha = 200 94 | local progress = math.min((CurTime() - g_VR.deathTime) / fadeDuration, 1) 95 | fadeAlpha = math.min(progress * maxAlpha, maxAlpha) 96 | cam.Start2D() 97 | surface.SetDrawColor(120, 0, 0, fadeAlpha) 98 | surface.DrawRect(0, 0, rtWidth, rtHeight) 99 | cam.End2D() 100 | end -------------------------------------------------------------------------------- /lua/vrmod/player/cl_laser_pointer.lua: -------------------------------------------------------------------------------- 1 | if CLIENT then 2 | local convars = vrmod.GetConvars() 3 | -- Default laser color 4 | local laserColor = Color(255, 0, 0, 255) 5 | -- Custom laser beam material with vertex color support 6 | local LaserMaterial = Material("cable/red") -- fallback 7 | do 8 | local matData = { 9 | ["$basetexture"] = "color/white", 10 | ["$additive"] = "1", -- Glowing effect 11 | ["$vertexcolor"] = "1", -- Use per-vertex color 12 | ["$vertexalpha"] = "1", -- Use per-vertex alpha 13 | ["$nocull"] = "1", -- Make it visible from both sides 14 | ["$ignorez"] = "0", -- Depth-aware (optional) 15 | } 16 | 17 | local success, customMat = pcall(CreateMaterial, "CustomLaserMaterial", "UnlitGeneric", matData) 18 | if success and customMat then LaserMaterial = customMat end 19 | end 20 | 21 | -- Glow sprite material 22 | local GlowSprite = Material("sprites/glow04_noz") 23 | -- Update laserColor from convar string 24 | local function UpdateLaserColor(colorString) 25 | local r, g, b, a = string.match(colorString, "(%d+),(%d+),(%d+),(%d+)") 26 | if not (r and g and b and a) then return end 27 | laserColor = Color(tonumber(r), tonumber(g), tonumber(b), tonumber(a)) 28 | end 29 | 30 | -- ConVar listener for dynamic updates 31 | vrmod.AddCallbackedConvar("vrmod_laser_color", nil, "255,0,0,255", nil, "", nil, nil, nil, function(newValue) UpdateLaserColor(newValue) end) 32 | -- Flicker width for beam animation 33 | local function getFlickerWidth() 34 | return 0.05 + math.abs(math.sin(CurTime() * 40)) * 0.05 35 | end 36 | 37 | -- Beam + glow rendering 38 | local function drawLaser() 39 | if not g_VR.viewModelMuzzle or g_VR.menuFocus then return end 40 | local wep = LocalPlayer():GetActiveWeapon() 41 | if not IsValid(wep) or g_VR.viewModelInfo[wep:GetClass()].noLaser then return end 42 | local startPos = g_VR.viewModelMuzzle.Pos 43 | local dir = g_VR.viewModelMuzzle.Ang:Forward() 44 | local endPos = startPos + dir * 10000 45 | local tr = util.TraceLine({ 46 | start = startPos, 47 | endpos = endPos, 48 | filter = LocalPlayer(), 49 | }) 50 | 51 | local function ScaleAlpha(col, scale) 52 | return Color(col.r, col.g, col.b, math.Clamp(col.a * scale, 0, 255)) 53 | end 54 | 55 | -- Draw laser beam 56 | render.SetMaterial(LaserMaterial) 57 | render.DrawBeam(startPos, tr.HitPos, getFlickerWidth(), 0, 1, laserColor) 58 | -- Draw muzzle glow (slightly smaller) 59 | render.SetMaterial(GlowSprite) 60 | render.DrawSprite(startPos, 1, 1, laserColor) 61 | -- Draw hit glow if beam hits something 62 | if tr.Hit then render.DrawSprite(tr.HitPos + tr.HitNormal * 1, 8, 8, ScaleAlpha(laserColor, 1.2)) end 63 | end 64 | 65 | local function setLaserEnabled(enabled) 66 | if enabled then 67 | hook.Add("PostDrawTranslucentRenderables", "vr_laserpointer", drawLaser) 68 | else 69 | hook.Remove("PostDrawTranslucentRenderables", "vr_laserpointer") 70 | end 71 | 72 | -- Persist state in convar 73 | RunConsoleCommand("vrmod_laserpointer", enabled and "1" or "0") 74 | end 75 | 76 | -- Console command to toggle laser 77 | concommand.Add("vrmod_togglelaserpointer", function() 78 | local enabled = GetConVar("vrmod_laserpointer"):GetBool() 79 | setLaserEnabled(not enabled) 80 | end) 81 | 82 | -- Activate laser if convar is set on VR start 83 | hook.Add("VRMod_Start", "laserOn", function() 84 | timer.Simple(0.1, function() 85 | if GetConVar("vrmod_laserpointer"):GetBool() then setLaserEnabled(true) end 86 | -- Force update laser color from current convar value 87 | local laserColorConvar = GetConVar("vrmod_laser_color") 88 | if laserColorConvar then UpdateLaserColor(laserColorConvar:GetString()) end 89 | end) 90 | end) 91 | end -------------------------------------------------------------------------------- /lua/vrmod/ui/cl_halos.lua: -------------------------------------------------------------------------------- 1 | if SERVER then return end 2 | local halos = {} 3 | local haloCount = 0 4 | local haloFrame = 0 5 | local haloRT, haloMat 6 | local localAng = Angle(0, -90, -90) 7 | local haloAdd_orig 8 | timer.Simple(0, function() haloAdd_orig = halo.Add end) 9 | hook.Add("VRMod_Start", "halos", function(ply) 10 | if ply ~= LocalPlayer() then return end 11 | halo.Add = function(ents, color, blurX, blurY, passes, additive, ignoreZ) 12 | if FrameNumber() ~= haloFrame then 13 | if FrameNumber() > haloFrame + 1 then 14 | --LocalPlayer():ChatPrint("installed halo hook") 15 | hook.Add("PostDrawTranslucentRenderables", "vrmod_halos", function(depth, sky) 16 | if not haloRT then 17 | haloRT = GetRenderTarget("3dhalos" .. tostring(math.floor(SysTime())), g_VR.view.w, g_VR.view.h, false) 18 | haloMat = CreateMaterial("3dhalos" .. tostring(math.floor(SysTime())), "UnlitGeneric", { 19 | ["$basetexture"] = haloRT:GetName() 20 | }) 21 | end 22 | 23 | if depth or sky or EyePos() ~= g_VR.view.origin then return end 24 | if FrameNumber() > haloFrame + 1 then 25 | haloCount = 0 26 | hook.Remove("PostDrawTranslucentRenderables", "vrmod_halos") 27 | --LocalPlayer():ChatPrint("removed halo hook") 28 | end 29 | 30 | -- 31 | render.PushRenderTarget(haloRT) 32 | render.Clear(0, 0, 0, 255, true, true) 33 | render.SetStencilEnable(true) 34 | render.SetStencilWriteMask(0xFF) 35 | render.SetStencilTestMask(0xFF) 36 | render.SetStencilPassOperation(STENCIL_REPLACE) 37 | render.SetStencilFailOperation(STENCIL_KEEP) 38 | render.SetStencilZFailOperation(STENCIL_KEEP) 39 | render.SetStencilReferenceValue(1) 40 | for i = 1, haloCount do 41 | for k, v in pairs(halos[i].ents) do 42 | if IsValid(v) then 43 | render.ClearStencil() 44 | render.SetStencilPassOperation(STENCIL_REPLACE) 45 | render.SetStencilCompareFunction(STENCIL_ALWAYS) 46 | v:DrawModel() 47 | render.SetStencilPassOperation(STENCIL_KEEP) 48 | render.SetStencilCompareFunction(STENCIL_EQUAL) 49 | local col = halos[i].color 50 | render.ClearBuffersObeyStencil(col.r, col.g, col.b, 255, false) 51 | end 52 | end 53 | end 54 | 55 | render.SetStencilEnable(false) 56 | render.BlurRenderTarget(haloRT, 2, 2, 1) 57 | render.SetStencilEnable(true) 58 | render.ClearBuffersObeyStencil(0, 0, 0, 255, false) 59 | render.SetStencilEnable(false) 60 | render.PopRenderTarget() 61 | local w = 10 * math.tan(math.rad(g_VR.view.fov / 2)) 62 | local h = w * 1 / g_VR.view.aspectratio 63 | local pos = EyePos() + EyeAngles():Forward() * 10 + EyeAngles():Right() * -w + EyeAngles():Up() * h 64 | local _, ang = LocalToWorld(Vector(0, 0, 0), localAng, Vector(0, 0, 0), EyeAngles()) 65 | local mtx = Matrix() 66 | mtx:Translate(pos) 67 | mtx:Rotate(ang) 68 | mtx:Scale(Vector(w * 2, h * 2, 0)) 69 | cam.PushModelMatrix(mtx) 70 | surface.SetDrawColor(255, 255, 255, 255) 71 | surface.SetMaterial(haloMat) 72 | render.OverrideBlend(true, BLEND_ONE, BLEND_ONE, BLENDFUNC_ADD, 0, 0, 0) 73 | render.OverrideDepthEnable(true, false) 74 | surface.DrawTexturedRect(0, 0, 1, 1) 75 | surface.DrawTexturedRect(0, 0, 1, 1) 76 | render.OverrideDepthEnable(false) 77 | render.OverrideBlend(false) 78 | cam.PopModelMatrix() 79 | -- 80 | end) 81 | end 82 | 83 | haloFrame = FrameNumber() 84 | haloCount = 0 85 | end 86 | 87 | haloEnts = ents 88 | halos[haloCount + 1] = { 89 | ents = ents, 90 | color = color 91 | } 92 | 93 | haloCount = haloCount + 1 94 | end 95 | end) 96 | 97 | hook.Add("VRMod_Exit", "halos", function(ply) 98 | if ply ~= LocalPlayer() then return end 99 | halo.Add = haloAdd_orig 100 | hook.Remove("PostDrawTranslucentRenderables", "vrmod_halos") 101 | haloCount = 0 102 | haloRT = nil 103 | end) -------------------------------------------------------------------------------- /lua/vrmod/utils/sh_trace.lua: -------------------------------------------------------------------------------- 1 | g_VR = g_VR or {} 2 | vrmod = vrmod or {} 3 | vrmod.utils = vrmod.utils or {} 4 | local magCache = {} 5 | local function IsMagazine(ent) 6 | local class = ent:GetClass() 7 | if magCache[class] ~= nil then return magCache[class] end 8 | local isMag = string.StartWith(class, "avrmag_") 9 | magCache[class] = isMag 10 | return isMag 11 | end 12 | 13 | --FILTERS AND TRACE UTILS 14 | function vrmod.utils.HitFilter(ent, ply, hand) 15 | if not IsValid(ent) then return false end 16 | if ent == ply then return end 17 | if ent:GetNWBool("isVRHand", false) then return false end 18 | if IsValid(ply) and (hand == "left" or hand == "right") then 19 | local held = vrmod.GetHeldEntity(ply, hand) 20 | if IsValid(held) and held == ent then return false end 21 | end 22 | return true 23 | end 24 | 25 | function vrmod.utils.MeleeFilter(ent, ply, hand) 26 | return vrmod.utils.HitFilter(ent, ply, hand) and not IsMagazine(ent) 27 | end 28 | 29 | function vrmod.utils.TraceHand(ply, hand, fromPalm) 30 | local startPos, ang, dir 31 | if hand == "left" then 32 | startPos = vrmod.GetLeftHandPos(ply) 33 | ang = vrmod.GetLeftHandAng(ply) 34 | if not ang then return nil end 35 | if fromPalm then 36 | dir = ang:Right() -- palm facing sideways 37 | startPos = startPos + ang:Up() * 2 -- offset upward from hand center 38 | else 39 | dir = Angle(ang.p, ang.y, ang.r + 180):Forward() -- original forward 40 | end 41 | else 42 | startPos = vrmod.GetRightHandPos(ply) 43 | ang = vrmod.GetRightHandAng(ply) 44 | if not ang then return nil end 45 | if fromPalm then 46 | dir = -ang:Right() -- palm facing sideways 47 | startPos = startPos + ang:Up() * 2 -- offset upward 48 | else 49 | dir = ang:Forward() -- normal forward 50 | end 51 | end 52 | 53 | if not startPos or not dir then return nil end 54 | dir:Normalize() 55 | local radius = fromPalm and 6 or 2 -- palm fatter, finger forward thinner 56 | local range = 32768 57 | local maxDepth = 10 58 | local ignore = {} 59 | local traceStart = startPos 60 | for i = 1, maxDepth do 61 | local tr = util.TraceHull({ 62 | start = traceStart, 63 | endpos = traceStart + dir * range, 64 | mins = Vector(-radius, -radius, -radius), 65 | maxs = Vector(radius, radius, radius), 66 | filter = ignore, 67 | mask = MASK_SHOT_HULL 68 | }) 69 | 70 | if not tr.Entity or not IsValid(tr.Entity) then return tr end 71 | if vrmod.utils.HitFilter(tr.Entity, ply, hand) then 72 | return tr 73 | else 74 | table.insert(ignore, tr.Entity) 75 | traceStart = tr.HitPos + dir * 1 -- nudge forward 76 | end 77 | end 78 | return nil 79 | end 80 | 81 | function vrmod.utils.TraceBoxOrSphere(data) 82 | local best = { 83 | Hit = false, 84 | Fraction = 1 85 | } 86 | 87 | if data.mins and data.maxs then 88 | -- Box trace 89 | local tr = util.TraceHull({ 90 | start = data.start, 91 | endpos = data.endpos, 92 | mins = data.mins, 93 | maxs = data.maxs, 94 | filter = data.filter, 95 | mask = data.mask 96 | }) 97 | 98 | if tr.Hit then 99 | best = tr 100 | best.Hit = true 101 | end 102 | else 103 | -- Sphere approximation with single hull trace 104 | local radius = data.radius or vrmod.DEFAULT_RADIUS 105 | local tr = util.TraceHull({ 106 | start = data.start, 107 | endpos = data.endpos, 108 | mins = Vector(-radius, -radius, -radius), 109 | maxs = Vector(radius, radius, radius), 110 | filter = data.filter, 111 | mask = data.mask 112 | }) 113 | 114 | if tr.Hit then 115 | best = tr 116 | best.Hit = true 117 | end 118 | end 119 | return best 120 | end -------------------------------------------------------------------------------- /lua/vrmod/ui/cl_worldtips.lua: -------------------------------------------------------------------------------- 1 | if SERVER then return end 2 | local tips = {} 3 | local localAng = Angle(0, -90, 90) 4 | local AddWorldTip_orig 5 | timer.Simple(0, function() AddWorldTip_orig = AddWorldTip end) 6 | hook.Add("VRMod_Start", "worldtips", function(ply) 7 | if ply ~= LocalPlayer() then return end 8 | AddWorldTip = function(entindex, text, dietime, pos, ent) 9 | if #tips == 0 then 10 | hook.Add("PostDrawTranslucentRenderables", "vrmod_worldtips", function(depth, sky) 11 | if depth or sky or EyePos() ~= g_VR.view.origin then return end 12 | local curtime = SysTime() 13 | surface.SetDrawColor(255, 255, 255, 255) 14 | cam.IgnoreZ(true) --$ignorez in material didn't seem to work 15 | local tms = render.GetToneMappingScaleLinear() 16 | render.SetToneMappingScaleLinear(Vector(0.75, 0.75, 0.75)) 17 | local i = 1 18 | while tips[i] do 19 | local v = tips[i] 20 | if curtime > v.dietime or not IsValid(v.ent) then 21 | table.remove(tips, i) 22 | continue 23 | end 24 | 25 | local pos = v.ent and v.ent:GetPos() or v.pos 26 | local scale = (g_VR.tracking.hmd.pos - pos):Length() * math.tan(0.0014) 27 | local pos, ang = LocalToWorld(Vector(0, scale * 512, scale * 512), localAng, pos, (pos - g_VR.tracking.hmd.pos):Angle()) 28 | cam.Start3D2D(pos, ang, scale) 29 | surface.SetMaterial(v.mat) 30 | surface.DrawTexturedRect(0, 0, 512, 512) 31 | cam.End3D2D() 32 | i = i + 1 33 | end 34 | 35 | cam.IgnoreZ(false) 36 | render.SetToneMappingScaleLinear(tms) 37 | if #tips == 0 then hook.Remove("PostDrawTranslucentRenderables", "vrmod_worldtips") end 38 | end) 39 | end 40 | 41 | local index = #tips + 1 42 | for i = 1, #tips do 43 | if tips[i].ent == ent or tips[i].pos == pos then 44 | index = i 45 | break 46 | end 47 | end 48 | 49 | if not tips[index] or tips[index].text ~= text then 50 | local rt = GetRenderTarget("worldtip" .. index, 512, 512, false) 51 | local mat = CreateMaterial("worldtip" .. index, "UnlitGeneric", { 52 | ["$basetexture"] = rt:GetName(), 53 | ["$translucent"] = 1 54 | }) 55 | 56 | render.PushRenderTarget(rt) 57 | render.ClearDepth() 58 | render.Clear(0, 0, 0, 0) 59 | cam.Start2D() 60 | local pos = { 61 | x = 512, 62 | y = 512 63 | } 64 | 65 | local black = Color(0, 0, 0, 255) 66 | local tipcol = Color(250, 250, 200, 255) 67 | local x = 0 68 | local y = 0 69 | local padding = 10 70 | local offset = 50 71 | surface.SetFont("GModWorldtip") 72 | local w, h = surface.GetTextSize(text) 73 | x = pos.x - w 74 | y = pos.y - h 75 | x = x - offset 76 | y = y - offset 77 | draw.RoundedBox(8, x - padding - 2, y - padding - 2, w + padding * 2 + 4, h + padding * 2 + 4, black) 78 | local verts = {} 79 | verts[1] = { 80 | x = x + w / 1.5 - 2, 81 | y = y + h + 2 82 | } 83 | 84 | verts[2] = { 85 | x = x + w + 2, 86 | y = y + h / 2 - 1 87 | } 88 | 89 | verts[3] = { 90 | x = pos.x - offset / 2 + 2, 91 | y = pos.y - offset / 2 + 2 92 | } 93 | 94 | draw.NoTexture() 95 | surface.SetDrawColor(0, 0, 0, tipcol.a) 96 | surface.DrawPoly(verts) 97 | draw.RoundedBox(8, x - padding, y - padding, w + padding * 2, h + padding * 2, tipcol) 98 | local verts = {} 99 | verts[1] = { 100 | x = x + w / 1.5, 101 | y = y + h 102 | } 103 | 104 | verts[2] = { 105 | x = x + w, 106 | y = y + h / 2 107 | } 108 | 109 | verts[3] = { 110 | x = pos.x - offset / 2, 111 | y = pos.y - offset / 2 112 | } 113 | 114 | draw.NoTexture() 115 | surface.SetDrawColor(tipcol.r, tipcol.g, tipcol.b, tipcol.a) 116 | surface.DrawPoly(verts) 117 | draw.DrawText(text, "GModWorldtip", x + w / 2, y, black, TEXT_ALIGN_CENTER) 118 | -- 119 | cam.End2D() 120 | render.PopRenderTarget() 121 | tips[index] = { 122 | text = text, 123 | pos = pos, 124 | ent = ent, 125 | mat = mat 126 | } 127 | end 128 | 129 | tips[index].dietime = SysTime() + 0.1 130 | end 131 | end) 132 | 133 | hook.Add("VRMod_Exit", "worldtips", function(ply) 134 | if ply ~= LocalPlayer() then return end 135 | AddWorldTip = AddWorldTip_orig 136 | hook.Remove("PostDrawTranslucentRenderables", "vrmod_worldtips") 137 | tips = {} 138 | end) -------------------------------------------------------------------------------- /lua/vrmod/utils/sh_frames.lua: -------------------------------------------------------------------------------- 1 | g_VR = g_VR or {} 2 | vrmod = vrmod or {} 3 | vrmod.utils = vrmod.utils or {} 4 | -- FRAME UTILS 5 | function vrmod.utils.CopyFrame(srcFrame) 6 | if not srcFrame then return nil end 7 | local copy = {} 8 | -- Copy primitive values directly 9 | copy.characterYaw = srcFrame.characterYaw 10 | -- Copy fingers 11 | for i = 1, 10 do 12 | copy["finger" .. i] = srcFrame["finger" .. i] 13 | end 14 | 15 | -- Helper for copying Vector/Angle safely 16 | local function copyPosAng(posKey, angKey) 17 | local pos = srcFrame[posKey] 18 | local ang = srcFrame[angKey] 19 | if pos then copy[posKey] = Vector(pos) end 20 | if ang then copy[angKey] = Angle(ang) end 21 | end 22 | 23 | -- Main tracked points 24 | copyPosAng("hmdPos", "hmdAng") 25 | copyPosAng("lefthandPos", "lefthandAng") 26 | copyPosAng("righthandPos", "righthandAng") 27 | -- Six point tracking, if present 28 | if srcFrame.waistPos or srcFrame.leftfootPos or srcFrame.rightfootPos then 29 | copyPosAng("waistPos", "waistAng") 30 | copyPosAng("leftfootPos", "leftfootAng") 31 | copyPosAng("rightfootPos", "rightfootAng") 32 | end 33 | return copy 34 | end 35 | 36 | function vrmod.utils.ConvertToRelativeFrame(absFrame) 37 | local lp = LocalPlayer() 38 | if not IsValid(lp) then return nil end 39 | local plyAng 40 | if lp:InVehicle() then 41 | local veh = lp:GetVehicle() 42 | if IsValid(veh) then 43 | plyAng = veh:GetAngles() 44 | else 45 | plyAng = Angle() 46 | end 47 | else 48 | plyAng = Angle() 49 | end 50 | 51 | local plyPos = lp:GetPos() 52 | local relFrame = { 53 | characterYaw = absFrame.characterYaw 54 | } 55 | 56 | -- Fingers 57 | for i = 1, 10 do 58 | relFrame["finger" .. i] = absFrame["finger" .. i] 59 | end 60 | 61 | local function convertPosAng(posKey, angKey) 62 | local pos = absFrame[posKey] 63 | local ang = absFrame[angKey] 64 | if pos and ang then 65 | local localPos, localAng = WorldToLocal(pos, ang, plyPos, plyAng) 66 | relFrame[posKey] = localPos 67 | relFrame[angKey] = localAng 68 | end 69 | end 70 | 71 | -- Main tracked points 72 | convertPosAng("hmdPos", "hmdAng") 73 | convertPosAng("lefthandPos", "lefthandAng") 74 | convertPosAng("righthandPos", "righthandAng") 75 | if g_VR.sixPoints then 76 | convertPosAng("waistPos", "waistAng") 77 | convertPosAng("leftfootPos", "leftfootAng") 78 | convertPosAng("rightfootPos", "rightfootAng") 79 | end 80 | return relFrame 81 | end 82 | 83 | function vrmod.utils.FramesAreEqual(f1, f2) 84 | if not f1 or not f2 then return false end 85 | local function equalVec(a, b) 86 | return vrmod.utils.VecAlmostEqual(a, b, 0.05) 87 | end 88 | 89 | local function equalAng(a, b) 90 | return vrmod.utils.AngAlmostEqual(a, b) 91 | end 92 | 93 | if f1.characterYaw ~= f2.characterYaw then return false end 94 | for i = 1, 10 do 95 | if f1["finger" .. i] ~= f2["finger" .. i] then return false end 96 | end 97 | 98 | if not equalVec(f1.hmdPos, f2.hmdPos) then return false end 99 | if not equalAng(f1.hmdAng, f2.hmdAng) then return false end 100 | if not equalVec(f1.lefthandPos, f2.lefthandPos) then return false end 101 | if not equalAng(f1.lefthandAng, f2.lefthandAng) then return false end 102 | if not equalVec(f1.righthandPos, f2.righthandPos) then return false end 103 | if not equalAng(f1.righthandAng, f2.righthandAng) then return false end 104 | if f1.waistPos then 105 | if not f2.waistPos then return false end 106 | if not equalVec(f1.waistPos, f2.waistPos) then return false end 107 | if not equalAng(f1.waistAng, f2.waistAng) then return false end 108 | if not equalVec(f1.leftfootPos, f2.leftfootPos) then return false end 109 | if not equalAng(f1.leftfootAng, f2.leftfootAng) then return false end 110 | if not equalVec(f1.rightfootPos, f2.rightfootPos) then return false end 111 | if not equalAng(f1.rightfootAng, f2.rightfootAng) then return false end 112 | end 113 | return true 114 | end -------------------------------------------------------------------------------- /lua/vrmod/physics/sh_collisions.lua: -------------------------------------------------------------------------------- 1 | if SERVER then 2 | CreateConVar("vrmod_collisions", "1", FCVAR_ARCHIVE + FCVAR_NOTIFY + FCVAR_REPLICATED, "Enable VR hand collision correction") 3 | util.AddNetworkString("vrmod_sync_model_params") 4 | net.Receive("vrmod_sync_model_params", function(len, ply) 5 | local modelPath = net.ReadString() 6 | local params = { 7 | radius = net.ReadFloat(), 8 | reach = net.ReadFloat(), 9 | mins_horizontal = net.ReadVector(), 10 | maxs_horizontal = net.ReadVector(), 11 | mins_vertical = net.ReadVector(), 12 | maxs_vertical = net.ReadVector(), 13 | angles = net.ReadAngle(), 14 | computed = true, 15 | sent = true 16 | } 17 | 18 | -- Only update + rebroadcast if different or unseen 19 | local old = vrmod.modelCache[modelPath] 20 | if not old or old.radius ~= params.radius or old.reach ~= params.reach or old.mins_horizontal ~= params.mins_horizontal or old.maxs_horizontal ~= params.maxs_horizontal or old.mins_vertical ~= params.mins_vertical or old.maxs_vertical ~= params.maxs_vertical or old.angles ~= params.angles then 21 | vrmod.logger.Info("Server received NEW collision params for %s from %s", modelPath, ply:Nick()) 22 | vrmod.modelCache[modelPath] = params 23 | net.Start("vrmod_sync_model_params") 24 | net.WriteString(modelPath) 25 | net.WriteFloat(params.radius) 26 | net.WriteFloat(params.reach) 27 | net.WriteVector(params.mins_horizontal) 28 | net.WriteVector(params.maxs_horizontal) 29 | net.WriteVector(params.mins_vertical) 30 | net.WriteVector(params.maxs_vertical) 31 | net.WriteAngle(params.angles) 32 | net.Broadcast() 33 | vrmod.logger.Info("Broadcasted collision params for %s to all clients", modelPath) 34 | else 35 | vrmod.logger.Info("Ignored duplicate collision params for %s from %s", modelPath, ply:Nick()) 36 | end 37 | end) 38 | 39 | hook.Add("PlayerInitialSpawn", "VRMod_Sendvrmod.modelCache", function(ply) 40 | for modelPath, params in pairs(vrmod.modelCache) do 41 | if params.computed then 42 | net.Start("vrmod_sync_model_params") 43 | net.WriteString(modelPath) 44 | net.WriteFloat(params.radius) 45 | net.WriteFloat(params.reach) 46 | net.WriteVector(params.mins_horizontal) 47 | net.WriteVector(params.maxs_horizontal) 48 | net.WriteVector(params.mins_vertical) 49 | net.WriteVector(params.maxs_vertical) 50 | net.WriteAngle(params.angles) 51 | net.Send(ply) 52 | vrmod.logger.Info("Synced cached collision params for %s to %s", modelPath, ply:Nick()) 53 | end 54 | end 55 | end) 56 | 57 | cvars.AddChangeCallback("vrmod_collisions", function(cvar, old, new) 58 | for _, ply in ipairs(player.GetAll()) do 59 | ply:SetNWBool("vrmod_server_enforce_collision", tobool(new)) 60 | end 61 | end) 62 | 63 | hook.Add("VRMod_Start", "SendCollisionState", function(ply) ply:SetNWBool("vrmod_server_enforce_collision", GetConVar("vrmod_collisions"):GetBool()) end) 64 | end 65 | 66 | if CLIENT then 67 | local cl_debug_collisions = CreateClientConVar("vrmod_debug_collisions", "0", true, FCVAR_CLIENTCMD_CAN_EXECUTE + FCVAR_ARCHIVE) 68 | net.Receive("vrmod_sync_model_params", function() 69 | local modelPath = net.ReadString() 70 | local params = { 71 | radius = net.ReadFloat(), 72 | reach = net.ReadFloat(), 73 | mins_horizontal = net.ReadVector(), 74 | maxs_horizontal = net.ReadVector(), 75 | mins_vertical = net.ReadVector(), 76 | maxs_vertical = net.ReadVector(), 77 | angles = net.ReadAngle(), 78 | computed = true 79 | } 80 | 81 | vrmod.logger.Debug("Received synced collision params for %s from server", modelPath) 82 | vrmod.modelCache[modelPath] = params 83 | end) 84 | 85 | hook.Add("PostDrawOpaqueRenderables", "VRMod_HandDebugShapes", function() 86 | if not cl_debug_collisions:GetBool() or not g_VR.active then return end 87 | local ply = LocalPlayer() 88 | if not IsValid(ply) or not ply:Alive() or not vrmod.IsPlayerInVR(ply) then return end 89 | render.SetColorMaterial() 90 | for i = 1, #vrmod.collisionSpheres do 91 | local s = vrmod.collisionSpheres[i] 92 | render.DrawWireframeSphere(s.pos, s.radius, 16, 16, s.hit and Color(255, 255, 0, 100) or Color(255, 0, 0, 150)) 93 | end 94 | 95 | for i = 1, #vrmod.collisionBoxes do 96 | local b = vrmod.collisionBoxes[i] 97 | render.DrawWireframeBox(b.pos, b.angles, b.mins, b.maxs, b.hit and Color(255, 255, 0, 100) or Color(0, 255, 0, 150)) 98 | end 99 | end) 100 | end -------------------------------------------------------------------------------- /lua/vrmod/ui/cl_actioneditor.lua: -------------------------------------------------------------------------------- 1 | if SERVER then return end 2 | g_VR = g_VR or {} 3 | g_VR.CustomActions = {} 4 | local open = false 5 | function VRUtilLoadCustomActions() 6 | local str = file.Read("vrmod/vrmod_custom_actions.txt") 7 | if str then g_VR.CustomActions = util.JSONToTable(str) end 8 | end 9 | 10 | concommand.Add("vrmod_actioneditor", function(ply, cmd, args) 11 | if open then return end 12 | open = true 13 | local window = vgui.Create("DFrame") 14 | window:SetPos(ScrW() / 2 - 350, ScrH() / 2 - 256) 15 | window:SetSize(700, 512) 16 | window:SetTitle("VRMod Custom Input Action Editor") 17 | window:MakePopup() 18 | local DLabel = vgui.Create("DLabel", window) 19 | DLabel:SetText("name [driving] concmd on press concmd on release") 20 | DLabel:SetPos(15, 31) 21 | DLabel:SizeToContents() 22 | function window:OnClose() 23 | open = false 24 | -- 25 | local i = 1 26 | local names = {} 27 | while i <= #g_VR.CustomActions do 28 | if g_VR.CustomActions[i][1] == "" or string.find(g_VR.action_manifest, "/" .. g_VR.CustomActions[i][1] .. "\"", 1, true) or names[g_VR.CustomActions[i][1]] then 29 | table.remove(g_VR.CustomActions, i) 30 | else 31 | names[g_VR.CustomActions[i][1]] = true 32 | i = i + 1 33 | end 34 | end 35 | 36 | -- 37 | file.Write("vrmod/vrmod_custom_actions.txt", util.TableToJSON(g_VR.CustomActions, false)) 38 | -- 39 | local pos = 0 40 | while true do 41 | local newPos = string.find(g_VR.action_manifest, "\"type\":", pos + 1) 42 | if not newPos then 43 | pos = string.find(g_VR.action_manifest, "}", pos) 44 | break 45 | end 46 | 47 | pos = newPos 48 | end 49 | 50 | local firstPart, lastPart = string.sub(g_VR.action_manifest, 1, pos), string.sub(g_VR.action_manifest, pos + 1) 51 | for i = 1, #g_VR.CustomActions do 52 | firstPart = firstPart .. ",\n {\n \"name\": \"" .. "/actions/" .. ((g_VR.CustomActions[i].driving or g_VR.CustomActions[i][4] == "1") and "driving" or "main") .. "/in/" .. g_VR.CustomActions[i][1] .. "\",\n \"type\": \"boolean\"\n }" 53 | end 54 | 55 | file.Write("vrmod/vrmod_action_manifest.txt", firstPart .. lastPart) 56 | end 57 | 58 | local DScrollPanel 59 | local function UpdateList(scrollTo) 60 | if DScrollPanel then 61 | DScrollPanel:Remove() 62 | DScrollPanel = nil 63 | end 64 | 65 | DScrollPanel = vgui.Create("DScrollPanel", window) 66 | DScrollPanel:SetSize(689, 451) 67 | DScrollPanel:SetPos(6, 56) 68 | for i = 1, #g_VR.CustomActions do 69 | local DPanel = DScrollPanel:Add("DPanel") 70 | DPanel:Dock(TOP) 71 | DPanel:DockMargin(0, 0, 0, 0) 72 | DPanel:SetSize(0, 25) 73 | DPanel:SetPaintBackground(false) 74 | --name 75 | local DTextEntry = vgui.Create("DTextEntry", DPanel) 76 | DTextEntry:SetPos(7, 0) 77 | DTextEntry:SetSize(110, 20) 78 | DTextEntry:SetValue(g_VR.CustomActions[i][1]) 79 | local validCharacters = "abcdefghijklmnopqrstuvwxyz0123456789_" 80 | DTextEntry.AllowInput = function(self, char) if not string.find(validCharacters, char, 1, true) then return true end end 81 | DTextEntry.OnChange = function(self) g_VR.CustomActions[i][1] = self:GetValue() end 82 | --use driving actionset? 83 | local DCheckBox = vgui.Create("DCheckBox", DPanel) 84 | DCheckBox:SetPos(122, 0) 85 | DCheckBox:SetValue(g_VR.CustomActions[i].driving or g_VR.CustomActions[i][4] == "1") 86 | DCheckBox.OnChange = function(self) g_VR.CustomActions[i][4] = self:GetValue() and "1" or "" end 87 | --cmd on press 88 | local DTextEntry = vgui.Create("DTextEntry", DPanel) 89 | DTextEntry:SetPos(7 + 130 + 7, 0) 90 | DTextEntry:SetSize(225, 20) 91 | DTextEntry:SetValue(g_VR.CustomActions[i][2]) 92 | DTextEntry.OnChange = function(self) g_VR.CustomActions[i][2] = self:GetValue() end 93 | --cmd release 94 | local DTextEntry = vgui.Create("DTextEntry", DPanel) 95 | DTextEntry:SetPos(7 + 130 + 7 + 225 + 7, 0) 96 | DTextEntry:SetSize(225, 20) 97 | DTextEntry:SetValue(g_VR.CustomActions[i][3]) 98 | DTextEntry.OnChange = function(self) g_VR.CustomActions[i][3] = self:GetValue() end 99 | --remove button 100 | local DButton = vgui.Create("DButton", DPanel) 101 | DButton:SetText("REMOVE") 102 | DButton:SetSize(54, 20) 103 | DButton:SetPos(608, 0) 104 | function DButton:DoClick() 105 | table.remove(g_VR.CustomActions, i) 106 | UpdateList(DScrollPanel:GetVBar():GetScroll()) 107 | end 108 | end 109 | 110 | timer.Simple(0, function() if IsValid(DScrollPanel) then DScrollPanel:GetVBar():SetScroll(scrollTo) end end) 111 | end 112 | 113 | local DButton = vgui.Create("DButton", window) 114 | DButton:SetText("ADD") 115 | DButton:SetSize(54, 20) 116 | DButton:SetPos(614, 31) 117 | function DButton:DoClick() 118 | g_VR.CustomActions[#g_VR.CustomActions + 1] = {"", "", "", ""} 119 | UpdateList(9999999) 120 | end 121 | 122 | VRUtilLoadCustomActions() 123 | UpdateList(0) 124 | end) -------------------------------------------------------------------------------- /lua/vrmod/ui/cl_hud.lua: -------------------------------------------------------------------------------- 1 | if SERVER then return end 2 | local vrScrH = CreateClientConVar("vrmod_ScrH_hud", ScrH(), true, FCVAR_ARCHIVE) 3 | local vrScrW = CreateClientConVar("vrmod_ScrW_hud", ScrW(), true, FCVAR_ARCHIVE) 4 | local function CurvedPlane(w, h, segments, degrees, matrix) 5 | matrix = matrix or Matrix() 6 | degrees = math.rad(degrees) 7 | local mesh = Mesh() 8 | local verts = {} 9 | local startAng = (math.pi - degrees) / 2 10 | local segLen = 0.5 * math.tan(degrees / segments) 11 | local scale = w / (segLen * segments) 12 | local zoffset = math.sin(startAng) * 0.5 * scale 13 | for i = 0, segments - 1 do 14 | local fraction = i / segments 15 | local nextFraction = (i + 1) / segments 16 | local ang1 = startAng + fraction * degrees 17 | local ang2 = startAng + nextFraction * degrees 18 | local x1 = math.cos(ang1) * -0.5 * scale 19 | local x2 = math.cos(ang2) * -0.5 * scale 20 | local z1 = math.sin(ang1) * 0.5 * scale - zoffset 21 | local z2 = math.sin(ang2) * 0.5 * scale - zoffset 22 | verts[#verts + 1] = { 23 | pos = matrix * Vector(x1, 0, z1), 24 | u = fraction, 25 | v = 0 26 | } 27 | 28 | verts[#verts + 1] = { 29 | pos = matrix * Vector(x2, 0, z2), 30 | u = nextFraction, 31 | v = 0 32 | } 33 | 34 | verts[#verts + 1] = { 35 | pos = matrix * Vector(x2, h, z2), 36 | u = nextFraction, 37 | v = 1 38 | } 39 | 40 | verts[#verts + 1] = { 41 | pos = matrix * Vector(x2, h, z2), 42 | u = nextFraction, 43 | v = 1 44 | } 45 | 46 | verts[#verts + 1] = { 47 | pos = matrix * Vector(x1, h, z1), 48 | u = fraction, 49 | v = 1 50 | } 51 | 52 | verts[#verts + 1] = { 53 | pos = matrix * Vector(x1, 0, z1), 54 | u = fraction, 55 | v = 0 56 | } 57 | end 58 | 59 | mesh:BuildFromTriangles(verts) 60 | return mesh 61 | end 62 | 63 | local rt = GetRenderTarget("vrmod_hud", vrScrW:GetInt(), vrScrH:GetInt(), false) 64 | local mat = Material("!vrmod_hud") 65 | mat = not mat:IsError() and mat or CreateMaterial("vrmod_hud", "UnlitGeneric", { 66 | ["$basetexture"] = rt:GetName(), 67 | ["$translucent"] = 1 68 | }) 69 | 70 | local hudMeshes = {} 71 | local hudMesh = nil 72 | local orig = nil 73 | local convars, convarValues = vrmod.GetConvars() 74 | local function RemoveHUD() 75 | hook.Remove("VRMod_PreRender", "hud") 76 | hook.Remove("HUDShouldDraw", "vrmod_hud") 77 | VRUtilRenderMenuSystem = orig or VRUtilRenderMenuSystem 78 | end 79 | 80 | local function AddHUD() 81 | RemoveHUD() 82 | if not g_VR.active or not convarValues.vrmod_hud then return end 83 | local mtx = Matrix() 84 | mtx:Translate(Vector(0, 0, vrScrH:GetInt() * convarValues.vrmod_hudscale / 2)) 85 | mtx:Rotate(Angle(0, -90, -90)) 86 | local meshName = convarValues.vrmod_hudscale .. "_" .. convarValues.vrmod_hudcurve 87 | hudMeshes[meshName] = hudMeshes[meshName] or CurvedPlane(vrScrW:GetInt() * convarValues.vrmod_hudscale, vrScrH:GetInt() * convarValues.vrmod_hudscale, 10, convarValues.vrmod_hudcurve, mtx) 88 | hudMesh = hudMeshes[meshName] 89 | local blacklist = {} 90 | for k, v in ipairs(string.Explode(",", convarValues.vrmod_hudblacklist)) do 91 | blacklist[v] = #v > 0 and true or blacklist[v] 92 | end 93 | 94 | if table.Count(blacklist) > 0 then hook.Add("HUDShouldDraw", "vrmod_hud", function(name) if blacklist[name] then return false end end) end 95 | hook.Add("VRMod_PreRender", "hud", function() 96 | if not g_VR.threePoints then return end 97 | render.PushRenderTarget(rt) 98 | render.OverrideAlphaWriteEnable(true, true) 99 | render.Clear(0, 0, 0, convarValues.vrmod_hudtestalpha, true, true) 100 | render.RenderHUD(0, 0, vrScrW:GetInt(), vrScrH:GetInt()) 101 | render.OverrideAlphaWriteEnable(false) 102 | render.PopRenderTarget() 103 | mtx:Identity() 104 | mtx:Translate(g_VR.tracking.hmd.pos + g_VR.tracking.hmd.ang:Forward() * convarValues.vrmod_huddistance) 105 | mtx:Rotate(g_VR.tracking.hmd.ang) 106 | end) 107 | 108 | --todo dont hook menu system to draw on top of player lol 109 | orig = orig or VRUtilRenderMenuSystem 110 | VRUtilRenderMenuSystem = function() 111 | render.SetMaterial(mat) 112 | cam.PushModelMatrix(mtx) 113 | render.DepthRange(0, 0.01) 114 | hudMesh:Draw() 115 | render.DepthRange(0, 1) 116 | cam.PopModelMatrix() 117 | orig() 118 | end 119 | end 120 | 121 | vrmod.AddCallbackedConvar("vrmod_hud", nil, 1, nil, nil, nil, nil, tobool, AddHUD) 122 | vrmod.AddCallbackedConvar("vrmod_hudblacklist", nil, "", nil, nil, nil, nil, nil, AddHUD) 123 | vrmod.AddCallbackedConvar("vrmod_hudcurve", nil, "60", nil, nil, nil, nil, tonumber, AddHUD) 124 | vrmod.AddCallbackedConvar("vrmod_hudscale", nil, "0.05", nil, nil, nil, nil, tonumber, AddHUD) 125 | vrmod.AddCallbackedConvar("vrmod_huddistance", nil, "60", nil, nil, nil, nil, tonumber) 126 | vrmod.AddCallbackedConvar("vrmod_hudtestalpha", nil, "0", nil, nil, nil, nil, tonumber) 127 | 128 | hook.Add("VRMod_Start", "hud", function(ply) 129 | if ply ~= LocalPlayer() then return end 130 | AddHUD() 131 | end) 132 | 133 | hook.Add("VRMod_Exit", "hud", function(ply) 134 | if ply ~= LocalPlayer() then return end 135 | RemoveHUD() 136 | end) -------------------------------------------------------------------------------- /lua/vrmod/pickup/sh_pickup_arcvr.lua: -------------------------------------------------------------------------------- 1 | local function init() 2 | if CLIENT then 3 | net.Receive("vrutil_net_pickup", function(len) 4 | local ply = net.ReadEntity() 5 | local ent = net.ReadEntity() 6 | local leftHand = net.ReadBool() 7 | local localPos = net.ReadVector() 8 | local localAng = net.ReadAngle() 9 | local steamid = ply:SteamID() 10 | if g_VR.net[steamid] == nil then return end 11 | -- 12 | ent.RenderOverride = function() 13 | if g_VR.net[steamid] == nil then return end 14 | local wpos, wang 15 | if leftHand then 16 | wpos, wang = LocalToWorld(localPos, localAng, g_VR.net[steamid].lerpedFrame.lefthandPos, g_VR.net[steamid].lerpedFrame.lefthandAng) 17 | else 18 | wpos, wang = LocalToWorld(localPos, localAng, g_VR.net[steamid].lerpedFrame.righthandPos, g_VR.net[steamid].lerpedFrame.righthandAng) 19 | end 20 | 21 | ent:SetPos(wpos) 22 | ent:SetAngles(wang) 23 | ent:SetupBones() 24 | ent:DrawModel() 25 | end 26 | 27 | ent.VRPickupRenderOverride = ent.RenderOverride 28 | --]] 29 | if ply == LocalPlayer() then 30 | if leftHand then 31 | g_VR.heldEntityLeft = ent 32 | else 33 | g_VR.heldEntityRight = ent 34 | end 35 | end 36 | 37 | hook.Call("VRMod_Pickup", nil, ply, ent) 38 | hook.Add("VRMod_Input", "arc_pickup_compat", function(action, pressed) 39 | if action == "boolean_left_pickup" and not pressed then 40 | net.Start("vrutil_net_drop") 41 | net.WriteBool(true) 42 | net.WriteVector(g_VR.tracking.pose_lefthand.pos) 43 | net.WriteAngle(g_VR.tracking.pose_lefthand.ang) 44 | net.SendToServer() 45 | g_VR.heldEntityLeft = nil 46 | hook.Remove("VRMod_Input", "arc_pickup_compat") 47 | end 48 | end) 49 | 50 | --notify server that arcvr pickups exist and we should run the position update thing 51 | net.Start("vrutil_net_pickup") 52 | net.SendToServer() 53 | end) 54 | 55 | net.Receive("vrutil_net_drop", function(len) 56 | local ply = net.ReadEntity() 57 | local ent = net.ReadEntity() 58 | if IsValid(ent) and ent.RenderOverride == ent.VRPickupRenderOverride then ent.RenderOverride = nil end 59 | hook.Call("VRMod_Drop", nil, ply, ent) 60 | end) 61 | elseif SERVER then 62 | util.AddNetworkString("vrutil_net_pickup") 63 | util.AddNetworkString("vrutil_net_drop") 64 | local function drop(ply, leftHand, handPos, handAng) 65 | for k, v in pairs(g_VR[ply:SteamID()].heldItems) do 66 | if v.left == leftHand then 67 | if IsValid(v.ent) and IsValid(v.ent:GetPhysicsObject()) and v.ent:GetPhysicsObject():IsMoveable() then 68 | local vel = v.ent:GetVelocity() 69 | local angvel = v.ent:GetPhysicsObject():GetAngleVelocity() 70 | if handPos and handAng then 71 | local wPos, wAng = LocalToWorld(v.localPos, v.localAng, handPos, handAng) 72 | v.ent:SetPos(wPos) 73 | v.ent:SetAngles(wAng) 74 | end 75 | 76 | v.ent:SetCollisionGroup(v.ent.originalCollisionGroup) 77 | v.ent:PhysicsInit(SOLID_VPHYSICS) 78 | v.ent:PhysWake() 79 | v.ent:GetPhysicsObject():SetVelocity(vel) 80 | v.ent:GetPhysicsObject():AddAngleVelocity(angvel) 81 | end 82 | 83 | net.Start("vrutil_net_drop") 84 | net.WriteEntity(ply) 85 | net.WriteEntity(v.ent) 86 | net.Broadcast() 87 | hook.Call("VRMod_Drop", nil, ply, v.ent) 88 | table.remove(g_VR[ply:SteamID()].heldItems, k) 89 | end 90 | end 91 | end 92 | 93 | vrmod.NetReceiveLimited("vrutil_net_pickup", 10, 0, function(len, ply) 94 | local tickrate = GetConVar("vrmod_net_tickrate"):GetInt() 95 | hook.Add("Tick", "arc_pickup_compat", function() 96 | local updates = false 97 | for k2, v2 in pairs(g_VR) do 98 | local ply = player.GetBySteamID(k2) 99 | if not IsValid(ply) then continue end 100 | local frame = v2.latestFrame 101 | for k, v in pairs(v2.heldItems) do 102 | if v.ply then --ignore if using new table structure 103 | continue 104 | end 105 | 106 | if not IsValid(v.ent) or not IsValid(v.ent:GetPhysicsObject()) or not v.ent:GetPhysicsObject():IsMoveable() or not ply:Alive() then 107 | drop(ply, v.left) 108 | continue 109 | end 110 | 111 | local handPos = LocalToWorld(v.left and frame.lefthandPos or frame.righthandPos, Angle(), ply:GetPos(), Angle()) 112 | local handAng = v.left and frame.lefthandAng or frame.righthandAng 113 | local wPos, wAng = LocalToWorld(v.localPos, v.localAng, handPos, handAng) 114 | v.targetPos = wPos 115 | v.ent:GetPhysicsObject():UpdateShadow(wPos, wAng, 1 / tickrate) 116 | updates = true 117 | end 118 | end 119 | 120 | if not updates then hook.Remove("Tick", "arc_pickup_compat") end 121 | end) 122 | end) 123 | 124 | vrmod.NetReceiveLimited("vrutil_net_drop", 10, 300, function(len, ply) 125 | local leftHand = net.ReadBool() 126 | local handPos = net.ReadVector() 127 | local handAng = net.ReadAngle() 128 | drop(ply, leftHand, handPos, handAng) 129 | end) 130 | 131 | hook.Add("VRMod_Start", "arc_pickup_compat", function(ply) g_VR[ply:SteamID()].heldItems = {} end) 132 | end 133 | end 134 | 135 | timer.Simple(0, function() if ArcticVR then init() end end) -------------------------------------------------------------------------------- /lua/vrmod/logger.lua: -------------------------------------------------------------------------------- 1 | vrmod = vrmod or {} 2 | vrmod.logger = vrmod.logger or {} 3 | vrmod.debug_cvars = vrmod.debug_cvars or {} 4 | -- ConVars for log levels 5 | local cv_console = CreateConVar("vrmod_log_console", "0", FCVAR_REPLICATED + FCVAR_ARCHIVE, "Minimum log level for console (0=OFF, 1=ERROR, 2=WARN, 3=INFO, 4=DEBUG)") 6 | local cv_file = CreateConVar("vrmod_log_file", "0", FCVAR_REPLICATED + FCVAR_ARCHIVE, "Minimum log level for file (0=OFF, 1=ERROR, 2=WARN, 3=INFO, 4=DEBUG)") 7 | -- Subsystem convars 8 | for subsystem, _ in pairs(vrmod.status or {}) do 9 | if subsystem ~= "api" then -- exclude the "api" subsystem 10 | local name = "vrmod_debug_" .. subsystem 11 | if not vrmod.debug_cvars[subsystem] then 12 | local convar = CreateConVar(name, "0", FCVAR_ARCHIVE + FCVAR_REPLICATED, "Enable debug for VRMod subsystem: " .. subsystem) 13 | vrmod.debug_cvars[subsystem] = convar 14 | end 15 | end 16 | end 17 | 18 | -- Log levels 19 | local levels = { 20 | ERROR = 1, 21 | WARN = 2, 22 | INFO = 3, 23 | DEBUG = 4, 24 | } 25 | 26 | -- Colors for log levels 27 | local levelColors = { 28 | DEBUG = Color(150, 150, 150), 29 | INFO = Color(0, 200, 0), 30 | WARN = Color(255, 200, 0), 31 | ERROR = Color(255, 0, 0), 32 | } 33 | 34 | -- Side colors 35 | local sideColors = { 36 | SERVER = Color(0, 150, 255), 37 | CLIENT = Color(255, 100, 255), 38 | } 39 | 40 | -- Subsystem colors 41 | local subsystemColors = {} -- dynamically assigned 42 | -- Generate a color for a subsystem 43 | local function getSubsystemColor(name) 44 | if subsystemColors[name] then return subsystemColors[name] end 45 | local r = math.random(50, 255) 46 | local g = math.random(50, 255) 47 | local b = math.random(50, 255) 48 | local col = Color(r, g, b) 49 | subsystemColors[name] = col 50 | return col 51 | end 52 | 53 | -- Helper to stringify values 54 | local function tostr(v) 55 | if istable(v) then 56 | local parts = {} 57 | for k, val in pairs(v) do 58 | table.insert(parts, tostring(k) .. "=" .. tostr(val)) 59 | end 60 | return "{" .. table.concat(parts, ", ") .. "}" 61 | elseif IsEntity and IsEntity(v) and v:IsValid() then 62 | return string.format("Entity[%s:%s]", v:GetClass(), v:EntIndex()) 63 | elseif IsEntity and IsEntity(v) then 64 | return "Entity[INVALID]" 65 | elseif isvector and isvector(v) then 66 | return string.format("Vector(%.2f, %.2f, %.2f)", v.x, v.y, v.z) 67 | elseif isangle and isangle(v) then 68 | return string.format("Angle(%.2f, %.2f, %.2f)", v.p, v.y, v.r) 69 | else 70 | return tostring(v) 71 | end 72 | end 73 | 74 | -- Subsystem detection 75 | local function detectSubsystem() 76 | local lvl = 3 77 | while true do 78 | local info = debug.getinfo(lvl, "S") 79 | if not info then break end 80 | if info.short_src then 81 | local src = string.lower(info.short_src) 82 | if src:find("vrmod/") and not src:find("vrmod/logger.lua") then 83 | local match = src:match("vrmod/([^/]+)/[^/]+%.lua$") 84 | if match and vrmod.status and vrmod.status[match] ~= nil then return match end 85 | end 86 | end 87 | 88 | lvl = lvl + 1 89 | end 90 | return "unknown" 91 | end 92 | 93 | -- File init 94 | local logDir = "vrmod_logs" 95 | if not file.Exists(logDir, "DATA") then file.CreateDir(logDir) end 96 | local side = SERVER and "SERVER" or "CLIENT" 97 | local logFileName = string.format("%s/vrmod_%s_%s.txt", logDir, side:lower(), os.date("%Y%m%d_%H%M%S")) 98 | local function writeFileLine(line) 99 | file.Append(logFileName, line .. "\n") 100 | end 101 | 102 | -- Core log function 103 | local function Log(level, fmt, ...) 104 | local lvlNum = levels[level] 105 | if not lvlNum then return end 106 | local subsystem = detectSubsystem() 107 | -- Subsystem-specific debug control 108 | if lvlNum == levels.DEBUG then 109 | local cv = vrmod.debug_cvars[subsystem] 110 | if not (cv and cv:GetBool()) then return end 111 | end 112 | 113 | -- Prepare message 114 | local msg 115 | if select("#", ...) > 0 then 116 | local args = {...} 117 | for i = 1, #args do 118 | args[i] = tostr(args[i]) 119 | end 120 | 121 | if type(fmt) == "string" and fmt:find("%%") then 122 | msg = string.format(fmt, unpack(args)) 123 | else 124 | table.insert(args, 1, fmt) 125 | msg = table.concat(args, " ") 126 | end 127 | else 128 | msg = tostr(fmt) 129 | end 130 | 131 | -- Console output 132 | if lvlNum <= cv_console:GetInt() then 133 | local colLvl = levelColors[level] or color_white 134 | local colSide = sideColors[side] or color_white 135 | local colSub = getSubsystemColor(subsystem) 136 | MsgC(colSide, "[VR][" .. side .. "]", colSub, "[" .. subsystem:upper() .. "]", colLvl, "[" .. level .. "]", color_white, msg .. "\n") 137 | end 138 | 139 | -- File output 140 | if lvlNum <= cv_file:GetInt() then 141 | local line = string.format("[VR][%s][%s][%s] %s", side, subsystem:upper(), level, msg) 142 | writeFileLine(os.date("[%H:%M:%S] ") .. line) 143 | end 144 | end 145 | 146 | -- Public shorthands 147 | function vrmod.logger.Debug(fmt, ...) 148 | Log("DEBUG", fmt, ...) 149 | end 150 | 151 | function vrmod.logger.Info(fmt, ...) 152 | Log("INFO", fmt, ...) 153 | end 154 | 155 | function vrmod.logger.Warn(fmt, ...) 156 | Log("WARN", fmt, ...) 157 | end 158 | 159 | function vrmod.logger.Err(fmt, ...) 160 | Log("ERROR", fmt, ...) 161 | end -------------------------------------------------------------------------------- /lua/vrmod/pickup/sh_dropweapon.lua: -------------------------------------------------------------------------------- 1 | local blacklist_path = "vrmod/vrmod_drop_blacklist.txt" 2 | -- Shared blacklist check 3 | local function InBlackList(weaponClass) 4 | if weaponClass == "weapon_vrmod_empty" then 5 | return true 6 | end 7 | if not file.Exists(blacklist_path, "DATA") then return false end 8 | local content = file.Read(blacklist_path, "DATA") or "" 9 | for line in string.gmatch(content, "[^\r\n]+") do 10 | if string.Trim(line) == weaponClass then return true end 11 | end 12 | return false 13 | end 14 | 15 | if SERVER then 16 | -- Create blacklist file with defaults if missing 17 | if not file.Exists(blacklist_path, "DATA") then 18 | local default_blacklist = {"weapon_fists", "piss_swep", "weapon_bsmod_punch", "weapon_vrmod_empty", "weapon_haax_vr", "alex_matrix_stopbullets", "blink", "spartan_kick", "arcticvr_nade_frag", "arcticvr_nade_flash", "arcticvr_nade_smoke"} 19 | file.Write(blacklist_path, table.concat(default_blacklist, "\n")) 20 | end 21 | 22 | util.AddNetworkString("ChangeWeapon") 23 | util.AddNetworkString("DropWeapon") 24 | util.AddNetworkString("SelectEmptyWeapon") 25 | net.Receive("ChangeWeapon", function(_, ply) 26 | local weaponClass = net.ReadString() 27 | if weaponClass and isstring(weaponClass) then ply:SelectWeapon(weaponClass) end 28 | end) 29 | 30 | net.Receive("SelectEmptyWeapon", function(_, ply) ply:SelectWeapon("weapon_vrmod_empty") end) 31 | net.Receive("DropWeapon", function(_, ply) 32 | local dropAsWeapon = net.ReadBool() 33 | local rhandvel = net.ReadVector() 34 | local rhandangvel = net.ReadVector() 35 | local wep = ply:GetActiveWeapon() 36 | if not IsValid(wep) or wep.undroppable or ply:InVehicle() or InBlackList(wep:GetClass()) then return end 37 | local modelname = wep:GetModel() 38 | local guninhandpos = vrmod.GetRightHandPos(ply) 39 | local guninhandang = vrmod.GetRightHandAng(ply) 40 | local dropEnt 41 | if dropAsWeapon then 42 | dropEnt = ents.Create(wep:GetClass()) 43 | else 44 | dropEnt = ents.Create("prop_physics") 45 | end 46 | 47 | -- Restore some ammo into dropped weapon if applicable 48 | local ammoType = wep:GetPrimaryAmmoType() 49 | local ammoCount = ply:GetAmmoCount(ammoType) 50 | local clipSize = wep:GetMaxClip1() 51 | local currentClip = wep:Clip1() 52 | if ammoCount > 0 and currentClip < clipSize then 53 | local ammoNeeded = clipSize - currentClip 54 | local ammoToGive = math.min(ammoNeeded, ammoCount) 55 | wep:SetClip1(currentClip + ammoToGive) 56 | ply:RemoveAmmo(ammoToGive, ammoType) 57 | end 58 | 59 | ply:Give("weapon_vrmod_empty") 60 | ply:SelectWeapon("weapon_vrmod_empty") 61 | local boneID = ply:LookupBone("ValveBiped.Bip01_R_Hand") 62 | local boneAng = boneID and select(2, ply:GetBonePosition(boneID)) or guninhandang 63 | dropEnt:SetModel(modelname) 64 | dropEnt:SetPos(guninhandpos + boneAng:Forward() * 10 + boneAng:Right() * 4) 65 | dropEnt:SetAngles(guninhandang) 66 | dropEnt:Spawn() 67 | local phys = dropEnt:GetPhysicsObject() 68 | if IsValid(phys) then 69 | phys:Wake() 70 | phys:SetMass(99) 71 | phys:SetVelocity(ply:GetVelocity() + rhandvel) 72 | phys:AddAngleVelocity(-phys:GetAngleVelocity() + phys:WorldToLocalVector(rhandangvel)) 73 | end 74 | 75 | if dropAsWeapon then ply:StripWeapon(wep:GetClass()) end 76 | timer.Simple(3, function() if IsValid(dropEnt) and dropEnt:GetClass() == "prop_physics" then dropEnt:Remove() end end) 77 | end) 78 | 79 | concommand.Add("vrmod_toggle_blacklist", function(ply) 80 | if not IsValid(ply) or not ply:IsPlayer() then return end 81 | local wep = ply:GetActiveWeapon() 82 | if not IsValid(wep) then 83 | ply:ChatPrint("[VRMod] No active weapon to toggle in blacklist.") 84 | return 85 | end 86 | 87 | local class = wep:GetClass() 88 | local lines = {} 89 | if file.Exists(blacklist_path, "DATA") then 90 | for line in string.gmatch(file.Read(blacklist_path, "DATA") or "", "[^\r\n]+") do 91 | table.insert(lines, string.Trim(line)) 92 | end 93 | end 94 | 95 | for i, v in ipairs(lines) do 96 | if v == class then 97 | table.remove(lines, i) 98 | file.Write(blacklist_path, table.concat(lines, "\n")) 99 | ply:ChatPrint("[VRMod] Removed '" .. class .. "' from blacklist.") 100 | return 101 | end 102 | end 103 | 104 | table.insert(lines, class) 105 | file.Write(blacklist_path, table.concat(lines, "\n")) 106 | ply:ChatPrint("[VRMod] Added '" .. class .. "' to blacklist.") 107 | end) 108 | end 109 | 110 | if CLIENT then 111 | local dropenable = CreateClientConVar("vrmod_weapondrop_enable", 1, true, FCVAR_ARCHIVE, "", 0, 1) 112 | hook.Add("VRMod_Input", "Weapon_Drop", function(action, state) 113 | if not dropenable:GetBool() or g_VR.antiDrop then return end 114 | if action == "boolean_right_pickup" and not state then 115 | net.Start("DropWeapon") 116 | net.WriteBool(true) 117 | net.WriteVector(vrmod.GetRightHandVelocity() * 2.5) 118 | net.WriteVector(vrmod.GetRightHandAngularVelocity() * 2.5) 119 | net.SendToServer() 120 | end 121 | end) 122 | end -------------------------------------------------------------------------------- /lua/vrmod/input/cl_keypad.lua: -------------------------------------------------------------------------------- 1 | if CLIENT then 2 | local maxDistance = 35 3 | local originalCalculateCursorPos = nil 4 | local keypadFocusedEnt = nil 5 | local unpatched = true 6 | local cursorVisible = false 7 | local cursorPos = { 8 | x = 0, 9 | y = 0 10 | } 11 | 12 | local mat_beam = Material("cable/redlaser") 13 | local function CalculateCursorPos_VR(self) 14 | local ply = LocalPlayer() 15 | if not IsValid(ply) then return 0, 0 end 16 | local handPos = vrmod.GetRightHandPos(ply) 17 | if not handPos then return 0, 0 end 18 | -- Distance check here too: prevent cursor if hand too far 19 | if handPos:Distance(self:GetPos()) > maxDistance then return 0, 0 end 20 | local tr = vrmod.utils.TraceHand(ply, "right") 21 | if not tr or not tr.Hit or tr.Entity ~= self then return 0, 0 end 22 | local scale = self.Scale or 1 23 | local pos, ang = self:CalculateRenderPos(), self:CalculateRenderAng() 24 | local normal = self:GetForward() 25 | local intersection = util.IntersectRayWithPlane(tr.HitPos, -tr.Normal, pos, normal) 26 | if not intersection then return 0, 0 end 27 | local diff = pos - intersection 28 | local x = diff:Dot(-ang:Forward()) / scale 29 | local y = diff:Dot(-ang:Right()) / scale 30 | return x, y 31 | end 32 | 33 | local function PatchKeypadEntity(ent) 34 | if not IsValid(ent) then return end 35 | if ent.__VRPatched then return end 36 | ent.__VRPatched = true 37 | if not originalCalculateCursorPos and ent.CalculateCursorPos then originalCalculateCursorPos = ent.CalculateCursorPos end 38 | ent.Mins = ent:OBBMins() 39 | ent.Maxs = ent:OBBMaxs() 40 | ent.CalculateCursorPos = CalculateCursorPos_VR 41 | end 42 | 43 | local function UnpatchKeypadEntity(ent) 44 | if IsValid(ent) and ent.__VRPatched then 45 | ent.__VRPatched = nil 46 | if originalCalculateCursorPos then ent.CalculateCursorPos = originalCalculateCursorPos end 47 | end 48 | end 49 | 50 | local function UnpatchAllKeypads() 51 | for _, ent in ipairs(ents.FindByClass("Keypad")) do 52 | UnpatchKeypadEntity(ent) 53 | end 54 | 55 | for _, ent in ipairs(ents.FindByClass("Keypad_Wire")) do 56 | UnpatchKeypadEntity(ent) 57 | end 58 | end 59 | 60 | local function UpdateKeypadInteraction() 61 | local ply = LocalPlayer() 62 | if not IsValid(ply) or not g_VR.active then 63 | keypadFocusedEnt = nil 64 | cursorVisible = false 65 | gui.EnableScreenClicker(false) 66 | UnpatchAllKeypads() 67 | return 68 | end 69 | 70 | local tr = vrmod.utils.TraceHand(ply, "right") 71 | if not tr or not tr.Hit or not IsValid(tr.Entity) then 72 | keypadFocusedEnt = nil 73 | cursorVisible = false 74 | gui.EnableScreenClicker(false) 75 | UnpatchAllKeypads() 76 | return 77 | end 78 | 79 | local ent = tr.Entity 80 | local class = ent:GetClass() 81 | if class ~= "Keypad" and class ~= "Keypad_Wire" then 82 | keypadFocusedEnt = nil 83 | cursorVisible = false 84 | gui.EnableScreenClicker(false) 85 | UnpatchAllKeypads() 86 | return 87 | end 88 | 89 | local handPos = vrmod.GetRightHandPos(ply) 90 | if not handPos or handPos:Distance(ent:GetPos()) > maxDistance then 91 | -- Unpatch when player too far away so normal interaction works 92 | UnpatchKeypadEntity(ent) 93 | keypadFocusedEnt = nil 94 | cursorVisible = false 95 | gui.EnableScreenClicker(false) 96 | return 97 | end 98 | 99 | PatchKeypadEntity(ent) 100 | keypadFocusedEnt = ent 101 | cursorVisible = true 102 | local x, y = ent:CalculateCursorPos() 103 | if x == 0 and y == 0 then 104 | cursorVisible = false 105 | gui.EnableScreenClicker(false) 106 | keypadFocusedEnt = nil 107 | return 108 | end 109 | 110 | cursorPos.x, cursorPos.y = x, y 111 | gui.EnableScreenClicker(true) 112 | end 113 | 114 | hook.Add("Think", "VRMod_Keypad_Interaction", function() 115 | if not g_VR or not g_VR.active then 116 | if not unpatched then 117 | UnpatchAllKeypads() 118 | keypadFocusedEnt = nil 119 | cursorVisible = false 120 | gui.EnableScreenClicker(false) 121 | unpatched = true 122 | end 123 | return 124 | end 125 | 126 | UpdateKeypadInteraction() 127 | unpatched = false 128 | end) 129 | 130 | hook.Add("PostDrawTranslucentRenderables", "VRMod_Keypad_Beam", function() 131 | if not cursorVisible or not IsValid(keypadFocusedEnt) then return end 132 | local ply = LocalPlayer() 133 | local handPos = vrmod.GetRightHandPos(ply) 134 | local tr = vrmod.utils.TraceHand(ply, "right") 135 | local hitPos = tr and tr.Hit and tr.HitPos or keypadFocusedEnt:GetPos() 136 | render.SetMaterial(mat_beam) 137 | render.DrawBeam(handPos, hitPos, 0.1, 0, 1, Color(255, 255, 255, 255)) 138 | end) 139 | 140 | hook.Add("VRMod_Input", "VRMod_Keypad_MouseInput", function(action, pressed) 141 | if not cursorVisible or not IsValid(keypadFocusedEnt) then return end 142 | local mouseButton = nil 143 | if action == "boolean_primaryfire" then mouseButton = MOUSE_LEFT end 144 | if mouseButton then 145 | if pressed then 146 | local hovered = keypadFocusedEnt:GetHoveredElement(cursorPos.x, cursorPos.y) 147 | if hovered and hovered.click then hovered.click(keypadFocusedEnt) end 148 | gui.InternalMousePressed(mouseButton) 149 | else 150 | gui.InternalMouseReleased(mouseButton) 151 | end 152 | end 153 | end) 154 | end -------------------------------------------------------------------------------- /lua/vrmod/ui/sh_numpad.lua: -------------------------------------------------------------------------------- 1 | if CLIENT then 2 | local open = false 3 | local wasClicking = false 4 | local justClicked = false 5 | local holdKey = nil 6 | local binder = nil 7 | local holdStart = 0 8 | local holdDelay = 0.5 9 | local holdRate = 0.1 10 | local keyMap = { 11 | ["1"] = KEY_PAD_1, 12 | ["2"] = KEY_PAD_2, 13 | ["3"] = KEY_PAD_3, 14 | ["4"] = KEY_PAD_4, 15 | ["5"] = KEY_PAD_5, 16 | ["6"] = KEY_PAD_6, 17 | ["7"] = KEY_PAD_7, 18 | ["8"] = KEY_PAD_8, 19 | ["9"] = KEY_PAD_9, 20 | ["0"] = KEY_PAD_0, 21 | ["CLR"] = KEY_BACKSPACE, 22 | ["ENT"] = KEY_PAD_ENTER, 23 | ["+"] = KEY_PAD_PLUS, 24 | ["-"] = KEY_PAD_MINUS, 25 | ["*"] = KEY_PAD_MULTIPLY, 26 | } 27 | 28 | local keys = {"1", "2", "3", "4", "5", "6", "7", "8", "9", "CLR", "0", "ENT", "+", "-", "*"} 29 | local function emitKey(name, down) 30 | local code = keyMap[name] 31 | if not code then return end 32 | net.Start("vrmod_numpad_emit") 33 | net.WriteUInt(code, 8) 34 | net.WriteBool(down) 35 | net.SendToServer() 36 | end 37 | 38 | local function FindCtrlNumPadUnderCursor() 39 | local hovered = vgui.GetHoveredPanel() 40 | if not IsValid(hovered) then return end 41 | local function IsChildOf(panel, parent) 42 | while IsValid(panel) do 43 | if panel == parent then return true end 44 | panel = panel:GetParent() 45 | end 46 | return false 47 | end 48 | 49 | local function FindNamedPanelAncestor(panel, targetName) 50 | while IsValid(panel) do 51 | if panel:GetName() == targetName then return panel end 52 | panel = panel:GetParent() 53 | end 54 | return nil 55 | end 56 | 57 | local ctrlPanel = FindNamedPanelAncestor(hovered, "CtrlNumPad") 58 | if not IsValid(ctrlPanel) then return end 59 | if IsValid(ctrlPanel.NumPad1) and IsChildOf(hovered, ctrlPanel.NumPad1) then 60 | binder = ctrlPanel.NumPad1 61 | elseif IsValid(ctrlPanel.NumPad2) and IsChildOf(hovered, ctrlPanel.NumPad2) then 62 | binder = ctrlPanel.NumPad2 63 | end 64 | end 65 | 66 | function VRUtilNumpadMenuOpen() 67 | if open then return end 68 | open = true 69 | VRUtilMenuOpen("numpadmenu", 512, 512, nil, true, Vector(6, -10, 5.5), Angle(0, -90, 55), 0.03, true, function() 70 | hook.Remove("PreRender", "vrutil_hook_rendernumpad") 71 | hook.Remove("VRMod_Input", "vrmod_numpad_clickdetect") 72 | hook.Remove("Think", "vrmod_numpad_holdrepeat") 73 | if holdKey then 74 | emitKey(holdKey, false) 75 | holdKey = nil 76 | end 77 | 78 | wasClicking = false 79 | justClicked = false 80 | open = false 81 | end) 82 | 83 | hook.Add("VRMod_Input", "vrmod_numpad_clickdetect", function(action, pressed) 84 | if action == "boolean_primaryfire" or action == "boolean_car_mouse_left" then 85 | justClicked = pressed and not wasClicking 86 | wasClicking = pressed 87 | FindCtrlNumPadUnderCursor() 88 | if not pressed and holdKey then 89 | emitKey(holdKey, false) 90 | holdKey = nil 91 | end 92 | end 93 | end) 94 | 95 | hook.Add("PreRender", "vrutil_hook_rendernumpad", function() 96 | if not VRUtilIsMenuOpen("numpadmenu") then return end 97 | if not g_VR.menuCursorX then return end 98 | local cx, cy = g_VR.menuCursorX, g_VR.menuCursorY 99 | local bw, bh, pad = 100, 100, 10 100 | local gridW = 3 * bw + 2 * pad 101 | local gridH = 5 * bh + 4 * pad 102 | local scaleX = 512 / gridW 103 | local scaleY = 512 / gridH 104 | local scale = math.min(scaleX, scaleY) 105 | bw = bw * scale 106 | bh = bh * scale 107 | pad = pad * scale 108 | VRUtilMenuRenderStart("numpadmenu") 109 | for i = 0, #keys - 1 do 110 | local col = i % 3 111 | local row = math.floor(i / 3) 112 | local x = col * (bw + pad) 113 | local y = row * (bh + pad) 114 | local key = keys[i + 1] 115 | local hovered = cx > x and cx < x + bw and cy > y and cy < y + bh and g_VR.menuFocus == "numpadmenu" 116 | draw.RoundedBox(8, x, y, bw, bh, Color(0, 0, 0, hovered and 200 or 100)) 117 | draw.SimpleText(key, "DermaLarge", x + bw / 2, y + bh / 2, color_white, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER) 118 | if hovered and justClicked then 119 | if IsValid(binder) then 120 | binder:SetSelectedNumber(keyMap[key]) 121 | binder:GetValue() 122 | binder = nil 123 | end 124 | 125 | emitKey(key, true) 126 | holdKey = key 127 | holdStart = SysTime() 128 | end 129 | end 130 | 131 | VRUtilMenuRenderEnd() 132 | justClicked = false 133 | end) 134 | 135 | hook.Add("Think", "vrmod_numpad_holdrepeat", function() 136 | if not holdKey then return end 137 | if not wasClicking then 138 | emitKey(holdKey, false) 139 | holdKey = nil 140 | return 141 | end 142 | 143 | local dt = SysTime() - holdStart 144 | if dt >= holdDelay then 145 | emitKey(holdKey, true) 146 | holdStart = holdStart + holdRate 147 | end 148 | end) 149 | end 150 | 151 | function VRUtilNumpadMenuClose() 152 | VRUtilMenuClose("numpadmenu") 153 | end 154 | 155 | concommand.Add("vrmod_numpad", function() 156 | if VRUtilIsMenuOpen("numpadmenu") then 157 | VRUtilNumpadMenuClose() 158 | else 159 | VRUtilNumpadMenuOpen() 160 | end 161 | end) 162 | end 163 | 164 | if SERVER then 165 | util.AddNetworkString("vrmod_numpad_emit") 166 | net.Receive("vrmod_numpad_emit", function(len, ply) 167 | local key = net.ReadUInt(8) 168 | local down = net.ReadBool() 169 | if down then 170 | numpad.Activate(ply, key) 171 | else 172 | numpad.Deactivate(ply, key) 173 | end 174 | end) 175 | end -------------------------------------------------------------------------------- /lua/vrmod/player/cl_character_hands.lua: -------------------------------------------------------------------------------- 1 | if CLIENT then 2 | local hands 3 | CreateClientConVar("vrmod_floatinghands_material", "models/c_arms_citizen_hand", true, FCVAR_ARCHIVE) 4 | CreateClientConVar("vrmod_floatinghands_model", "models/player/vr_hands.mdl", true, FCVAR_ARCHIVE) 5 | local convars = vrmod.GetConvars() 6 | hook.Add("VRMod_Start", "vrmod_starthandsonly", function(ply) 7 | if not (ply == LocalPlayer() and convars.vrmod_floatinghands:GetBool()) then return end 8 | timer.Simple(0, function() LocalPlayer().RenderOverride = function() end end) 9 | local zeroVec, zeroAng = Vector(), Angle() 10 | local steamid = LocalPlayer():SteamID() 11 | hands = ClientsideModel(GetConVar("vrmod_floatinghands_model"):GetString()) 12 | hands:SetupBones() 13 | g_VR.hands = hands 14 | hands:SetMaterial(GetConVar("vrmod_floatinghands_material"):GetString()) 15 | local leftHand = hands:LookupBone("ValveBiped.Bip01_L_Hand") 16 | local rightHand = hands:LookupBone("ValveBiped.Bip01_R_Hand") 17 | local fingerboneids = {} 18 | local tmp = {"0", "01", "02", "1", "11", "12", "2", "21", "22", "3", "31", "32", "4", "41", "42"} 19 | for i = 1, 30 do 20 | fingerboneids[#fingerboneids + 1] = hands:LookupBone("ValveBiped.Bip01_" .. (i < 16 and "L" or "R") .. "_Finger" .. tmp[i - (i < 16 and 0 or 15)]) or -1 21 | end 22 | 23 | local boneinfo = {} 24 | local boneCount = hands:GetBoneCount() 25 | for i = 0, boneCount - 1 do 26 | local parent = hands:GetBoneParent(i) 27 | local mtx = hands:GetBoneMatrix(i) or Matrix() 28 | local mtxParent = hands:GetBoneMatrix(parent) or mtx 29 | local relativePos, relativeAng = WorldToLocal(mtx:GetTranslation(), mtx:GetAngles(), mtxParent:GetTranslation(), mtxParent:GetAngles()) 30 | boneinfo[i] = { 31 | name = hands:GetBoneName(i), 32 | parent = parent, 33 | relativePos = relativePos, 34 | relativeAng = relativeAng, 35 | offsetAng = zeroAng, 36 | pos = zeroVec, 37 | ang = zeroAng, 38 | targetMatrix = mtx 39 | } 40 | end 41 | 42 | hands:SetPos(LocalPlayer():GetPos()) 43 | hands:SetRenderBounds(zeroVec, zeroVec, Vector(1, 1, 1) * 65000) 44 | local frame = 0 45 | hands:AddCallback("BuildBonePositions", function(ent, numbones) 46 | if frame ~= FrameNumber() then 47 | frame = FrameNumber() 48 | if LocalPlayer():InVehicle() and LocalPlayer():GetVehicle():GetClass() ~= "prop_vehicle_prisoner_pod" then 49 | hands:AddEffects(EF_NODRAW) --note: this will block BuildBonePositions from running 50 | hook.Add("VRMod_ExitVehicle", "vrmod_floatinghands", function() 51 | hook.Remove("VRMod_ExitVehicle", "vrmod_floatinghands") 52 | hands:RemoveEffects(EF_NODRAW) 53 | end) 54 | return 55 | end 56 | 57 | local netFrame = g_VR.net[steamid] and g_VR.net[steamid].lerpedFrame 58 | if netFrame then 59 | boneinfo[leftHand].overridePos, boneinfo[leftHand].overrideAng = netFrame.lefthandPos, netFrame.lefthandAng 60 | boneinfo[rightHand].overridePos, boneinfo[rightHand].overrideAng = netFrame.righthandPos, netFrame.righthandAng + Angle(0, 0, 180) 61 | for k, v in pairs(fingerboneids) do 62 | if not boneinfo[v] then continue end 63 | boneinfo[v].offsetAng = LerpAngle(netFrame["finger" .. math.floor((k - 1) / 3 + 1)], g_VR.openHandAngles[k], g_VR.closedHandAngles[k]) 64 | end 65 | 66 | hands:SetPos(LocalPlayer():GetPos()) --for lighting 67 | end 68 | 69 | for i = 0, boneCount - 1 do 70 | local info = boneinfo[i] 71 | local parentInfo = boneinfo[info.parent] or info 72 | local wpos, wang = LocalToWorld(info.relativePos, info.relativeAng + info.offsetAng, parentInfo.pos, parentInfo.ang) 73 | wpos = info.overridePos or wpos 74 | wang = info.overrideAng or wang 75 | local mat = Matrix() 76 | mat:Translate(wpos) 77 | mat:Rotate(wang) 78 | info.targetMatrix = mat 79 | info.pos = wpos 80 | info.ang = wang 81 | end 82 | end 83 | 84 | for i = 0, boneCount - 1 do 85 | if hands:GetBoneMatrix(i) then hands:SetBoneMatrix(i, boneinfo[i].targetMatrix) end 86 | end 87 | end) 88 | 89 | g_VR = g_VR or {} 90 | g_VR.characterYaw = 0 91 | local convars, convarValues = vrmod.GetConvars() 92 | if not g_VR.threePoints or VRUtilIsMenuOpen("heightmenu") then return end 93 | --create mirror 94 | rt_mirror = GetRenderTarget("rt_vrmod_heightcalmirror", 2048, 2048) 95 | mat_mirror = CreateMaterial("mat_vrmod_heightcalmirror", "Core_DX90", { 96 | ["$basetexture"] = "rt_vrmod_heightcalmirror", 97 | ["$model"] = "1" 98 | }) 99 | 100 | local mirrorYaw = 0 101 | hook.Add("PreDrawTranslucentRenderables", "vrmod_floatinghands_dummymirror", function(depth, skybox) 102 | if depth or skybox or not (EyePos() == g_VR.eyePosLeft or EyePos() == g_VR.eyePosRight) then return end 103 | local ad = math.AngleDifference(EyeAngles().yaw, mirrorYaw) 104 | if math.abs(ad) > 45 then mirrorYaw = mirrorYaw + (ad > 0 and 45 or -45) end 105 | local mirrorPos = Vector(g_VR.tracking.hmd.pos.x, g_VR.tracking.hmd.pos.y, g_VR.origin.z + 45) + Angle(0, mirrorYaw, 0):Forward() * -5 106 | local mirrorAng = Angle(0, mirrorYaw - 90, 90) 107 | -- g_VR.menus.heightmenu.pos = mirrorPos + Vector(0,0,30) + mirrorAng:Forward()*-15 108 | -- g_VR.menus.heightmenu.ang = mirrorAng 109 | local camPos = LocalToWorld(WorldToLocal(EyePos(), Angle(), mirrorPos, mirrorAng) * Vector(1, 1, -1), Angle(), mirrorPos, mirrorAng) 110 | local camAng = EyeAngles() 111 | camAng = Angle(camAng.pitch, mirrorAng.yaw + mirrorAng.yaw - camAng.yaw, 180 - camAng.roll) 112 | cam.Start({ 113 | x = 0, 114 | y = 0, 115 | w = 2048, 116 | h = 2048, 117 | type = "3D", 118 | fov = g_VR.view.fov, 119 | aspect = -g_VR.view.aspectratio, 120 | origin = camPos, 121 | angles = camAng 122 | }) 123 | 124 | render.PushRenderTarget(rt_mirror) 125 | render.Clear(200, 230, 255, 0, true, true) 126 | render.CullMode(1) 127 | local alloworig = g_VR.allowPlayerDraw 128 | g_VR.allowPlayerDraw = true 129 | cam.Start3D() 130 | cam.End3D() 131 | local ogEyePos = EyePos 132 | EyePos = function() return Vector(0, 0, 0) end 133 | local ogRenderOverride = LocalPlayer().RenderOverride 134 | LocalPlayer().RenderOverride = nil 135 | render.SuppressEngineLighting(true) 136 | LocalPlayer():DrawModel() 137 | render.SuppressEngineLighting(false) 138 | EyePos = ogEyePos 139 | LocalPlayer().RenderOverride = ogRenderOverride 140 | g_VR.allowPlayerDraw = alloworig 141 | cam.Start3D() 142 | cam.End3D() 143 | render.CullMode(0) 144 | render.PopRenderTarget() 145 | cam.End3D() 146 | render.SetMaterial(mat_mirror) 147 | render.DrawQuadEasy(mirrorPos, mirrorAng:Up(), 1, 1, Color(255, 255, 255, 255), 0) 148 | end) 149 | end) 150 | 151 | hook.Add("VRMod_Exit", "vrmod_stophandsonly", function(ply, steamid) 152 | if IsValid(hands) then 153 | hands:Remove() 154 | LocalPlayer().RenderOverride = nil 155 | hook.Remove("PreDrawTranslucentRenderables", "vrmod_floatinghands_dummymirror") 156 | end 157 | end) 158 | end -------------------------------------------------------------------------------- /lua/vrmod/pickup/sv_weaponreplacer.lua: -------------------------------------------------------------------------------- 1 | vrmod = vrmod or {} 2 | vrmod.utils = vrmod.utils or {} 3 | if SERVER then 4 | util.AddNetworkString("VRWeps_Notify") 5 | local replacer = {} 6 | local originalWeapons = {} 7 | local datafile = "vrmod/vrmod_weapon_replacement.txt" 8 | local convar_enabled = CreateConVar("vrmod_weapon_swap", "1", FCVAR_ARCHIVE, "Enable or disable VR weapon replacement logic") 9 | local defaultWeaponPairs = { 10 | ["weapon_crowbar"] = "arcticvr_hl2_crowbar", 11 | ["weapon_stunstick"] = "arcticvr_hl2_stunstick", 12 | ["weapon_pistol"] = "arcticvr_hl2_pistol", 13 | ["weapon_357"] = "arcticvr_hl2_357", 14 | ["weapon_smg1"] = "arcticvr_hl2_smg1", 15 | ["weapon_annabelle"] = "arcticvr_hl2_annabelle", 16 | ["weapon_ar2"] = "arcticvr_hl2_ar2", 17 | ["weapon_shotgun"] = "arcticvr_hl2_shotgun", 18 | ["weapon_crossbow"] = "arcticvr_hl2_crossbow", 19 | ["weapon_rpg"] = "arcticvr_hl2_rpg", 20 | ["weapon_vj_9mmpistol"] = "arcticvr_hl2_pistol", 21 | ["weapon_vj_357"] = "arcticvr_hl2_357", 22 | ["weapon_vj_hlr2_alyxgun"] = "arcticvr_hl2_alyxgun", 23 | ["weapon_vj_hlr2_csniper"] = "arcticvr_hl2_cmbsniper", 24 | ["weapon_vj_smg1q"] = "arcticvr_hl2_smg1", 25 | ["weapon_vj_ar2"] = "arcticvr_hl2_ar2", 26 | ["weapon_vj_spas12"] = "arcticvr_hl2_shotgun", 27 | ["weapon_vj_ak47"] = "arcticvr_ak47", 28 | ["weapon_vj_senassault"] = "arcticvr_ak47", 29 | ["weapon_vj_rpg"] = "arcticvr_rpg", 30 | ["weapon_vj_glock17"] = "arcticvr_aniv_glock", 31 | ["weapon_vj_senpistol"] = "arcticvr_aniv_glock", 32 | ["weapon_vj_senpistolcopgloc"] = "arcticvr_aniv_glock", 33 | ["weapon_vj_m16a1"] = "arcticvr_m4a1", 34 | ["weapon_vj_sendeagle"] = "arcticvr_aniv_deagle", 35 | ["weapon_vj_sensmg"] = "arcticvr_aniv_mac10", 36 | ["weapon_vj_sensmgcop"] = "arcticvr_mp5", 37 | ["css_aug"] = "arcticvr_aug", 38 | ["css_awp"] = "arcticvr_awm", 39 | ["css_ak47"] = "arcticvr_ak47", 40 | ["css_deagle"] = "arcticvr_aniv_deagle", 41 | ["css_dualellites"] = "arcticvr_aniv_m9", 42 | ["css_famas"] = "arcticvr_famas", 43 | ["css_57"] = "arcticvr_fiveseven", 44 | ["css_g3sg1"] = "arcticvr_g3sg1", 45 | ["css_galil"] = "arcticvr_galil", 46 | ["css_glock"] = "arcticvr_aniv_glock", 47 | ["css_knife"] = "arcticvr_knife", 48 | ["css_m4a1"] = "arcticvr_m4a1", 49 | ["css_mac10"] = "arcticvr_aniv_mac10", 50 | ["css_mp5"] = "arcticvr_mp5", 51 | ["css_p90"] = "arcticvr_p90", 52 | ["css_scout"] = "arcticvr_scout", 53 | ["css_sg550"] = "arcticvr_sg552q", 54 | ["css_sg552"] = "arcticvr_sg552", 55 | ["css_m3"] = "arcticvr_shorty", 56 | ["css_tmp"] = "arcticvr_aniv_tmp", 57 | ["css_ump45"] = "arcticvr_ump45", 58 | ["css_usp"] = "arcticvr_aniv_usptactical", 59 | ["css_xm1014"] = "arcticvr_m1014", 60 | ["weapon_haax"] = "weapon_haax_vr", 61 | ["spooderman_swep"] = "vr_spooderman" 62 | } 63 | 64 | local function SavePairs() 65 | file.Write(datafile, util.TableToJSON(replacer, true)) 66 | end 67 | 68 | local function LoadPairs() 69 | if not file.Exists(datafile, "DATA") then 70 | vrmod.logger.Info("[VRWeps] No file found — writing default table.") 71 | file.Write(datafile, util.TableToJSON(defaultWeaponPairs, true)) 72 | replacer = table.Copy(defaultWeaponPairs) 73 | return 74 | end 75 | 76 | local raw = file.Read(datafile, "DATA") 77 | local tbl = util.JSONToTable(raw) or {} 78 | for flat, vr in pairs(defaultWeaponPairs) do 79 | if not tbl[flat] then 80 | vrmod.logger.Info("[VRWeps] Adding missing default pair:", flat, "→", vr) 81 | tbl[flat] = vr 82 | end 83 | end 84 | 85 | replacer = tbl 86 | SavePairs() 87 | vrmod.logger.Info("[VRWeps] Loaded", table.Count(replacer), "weapon pairs.") 88 | end 89 | 90 | local function AddPair(flat, vr) 91 | replacer[flat] = vr 92 | SavePairs() 93 | vrmod.logger.Info("[VRWeps] Added pair:", flat, "→", vr) 94 | end 95 | 96 | function vrmod.utils.ReplaceWeapon(ply, thing) 97 | if not IsValid(ply) or not IsValid(thing) then return false end 98 | if not convar_enabled:GetBool() then return false end 99 | if not vrmod.IsPlayerInVR(ply) then return false end 100 | local isWorldWeapon = thing:GetOwner() == NULL 101 | local class = thing:GetClass() 102 | local vrClass = replacer[class] 103 | if not vrClass then return false end 104 | if not weapons.GetStored(vrClass) then 105 | vrmod.logger.Info("[VRWeps] Missing VR weapon:", vrClass, "for", class) 106 | return false 107 | end 108 | 109 | local sid = ply:SteamID() 110 | originalWeapons[sid] = originalWeapons[sid] or {} 111 | table.insert(originalWeapons[sid], { 112 | from = vrClass, 113 | to = class 114 | }) 115 | 116 | -- Replace 117 | ply:Give(vrClass, true) 118 | if isWorldWeapon then 119 | -- Picked up from the ground 120 | if IsValid(thing) then thing:Remove() end 121 | else 122 | -- Already in inventory 123 | if ply:HasWeapon(class) then if engine.ActiveGamemode() ~= "lambda" then ply:StripWeapon(class) end end 124 | end 125 | return vrClass 126 | end 127 | 128 | local function ReplaceAllWeapons(ply) 129 | if not IsValid(ply) then return end 130 | originalWeapons[ply:SteamID()] = {} -- clear and re-track 131 | for _, wep in ipairs(ply:GetWeapons()) do 132 | vrmod.utils.ReplaceWeapon(ply, wep) 133 | end 134 | end 135 | 136 | local function RestoreFlatWeapons(ply) 137 | if not IsValid(ply) or not convar_enabled:GetBool() then return end 138 | local sid = ply:SteamID() 139 | local stored = originalWeapons[sid] 140 | if not stored then return end 141 | for _, info in ipairs(stored) do 142 | if ply:HasWeapon(info.from) then 143 | ply:Give(info.to, true) 144 | ply:StripWeapon(info.from) 145 | vrmod.logger.Info("[VRWeps] Restored:", info.to, "←", info.from) 146 | else 147 | vrmod.logger.Info("[VRWeps] Player doesn't have VR weapon:", info.from) 148 | end 149 | end 150 | 151 | originalWeapons[sid] = nil 152 | end 153 | 154 | concommand.Add("vrweps_addreplace", function(ply, cmd, args) 155 | if IsValid(ply) and not ply:IsAdmin() then 156 | ply:ChatPrint("You must be admin to use this.") 157 | return 158 | end 159 | 160 | if #args < 2 then 161 | print("Usage: vrweps_addreplace ") 162 | return 163 | end 164 | 165 | AddPair(args[1], args[2]) 166 | end) 167 | 168 | -- Replace on VR start 169 | hook.Add("VRMod_Start", "VRWeps_ReplaceOnStart", function(ply) timer.Simple(0.1, function() ReplaceAllWeapons(ply) end) end) 170 | -- Restore on VR exit 171 | hook.Add("VRMod_Exit", "VRWeps_RestoreOnExit", function(ply) timer.Simple(0.1, function() RestoreFlatWeapons(ply) end) end) 172 | -- Replace on respawn 173 | hook.Add("PlayerSpawn", "VRWeps_ReplaceOnRespawn", function(ply) timer.Simple(0.1, function() ReplaceAllWeapons(ply) end) end) 174 | -- Load replacements at startup 175 | hook.Add("Initialize", "VRWeps_LoadPairs", function() LoadPairs() end) 176 | end -------------------------------------------------------------------------------- /lua/vrmod/ui/cl_heightadjust.lua: -------------------------------------------------------------------------------- 1 | if SERVER then return end 2 | local convars, convarValues = vrmod.GetConvars() 3 | local rt_mirror, mat_mirror = nil, nil 4 | local function AutoScale() 5 | g_VR.scale = 66.8 / ((g_VR.tracking.hmd.pos.z - g_VR.origin.z) / g_VR.scale) 6 | convars.vrmod_scale:SetFloat(g_VR.scale) 7 | end 8 | 9 | local function RenderMirror() 10 | if not g_VR or not g_VR.tracking then return end 11 | rt_mirror = rt_mirror or GetRenderTarget("rt_vrmod_heightcalmirror", 2048, 2048) 12 | mat_mirror = mat_mirror or CreateMaterial("mat_vrmod_heightcalmirror", "Core_DX90", { 13 | ["$basetexture"] = "rt_vrmod_heightcalmirror", 14 | ["$model"] = "1" 15 | }) 16 | 17 | local mirrorYaw = 0 18 | hook.Add("PreDrawTranslucentRenderables", "vrmodheightmirror", function(depth, skybox) 19 | if depth or skybox or not (EyePos() == g_VR.eyePosLeft or EyePos() == g_VR.eyePosRight) then return end 20 | local ad = math.AngleDifference(EyeAngles().yaw, mirrorYaw) 21 | if math.abs(ad) > 45 then mirrorYaw = mirrorYaw + (ad > 0 and 45 or -45) end 22 | local mirrorPos = Vector(g_VR.tracking.hmd.pos.x, g_VR.tracking.hmd.pos.y, g_VR.origin.z + 45) 23 | mirrorPos:Add(Angle(0, mirrorYaw, 0):Forward() * 50) 24 | local mirrorAng = Angle(0, mirrorYaw - 90, 90) 25 | if not g_VR.menus.heightmenu then return end 26 | g_VR.menus.heightmenu.pos = mirrorPos + Vector(0, 0, 30) + mirrorAng:Forward() * -15 27 | g_VR.menus.heightmenu.ang = mirrorAng 28 | local camPos = LocalToWorld(WorldToLocal(EyePos(), Angle(), mirrorPos, mirrorAng) * Vector(1, 1, -1), Angle(), mirrorPos, mirrorAng) 29 | local camAng = EyeAngles() 30 | camAng = Angle(camAng.pitch, mirrorAng.yaw + mirrorAng.yaw - camAng.yaw, 180 - camAng.roll) 31 | cam.Start({ 32 | x = 0, 33 | y = 0, 34 | w = 2048, 35 | h = 2048, 36 | type = "3D", 37 | fov = g_VR.view.fov, 38 | aspect = -g_VR.view.aspectratio, 39 | origin = camPos, 40 | angles = camAng 41 | }) 42 | 43 | render.PushRenderTarget(rt_mirror) 44 | render.Clear(200, 230, 255, 0, true, true) 45 | render.CullMode(1) 46 | local allowOrig = g_VR.allowPlayerDraw 47 | g_VR.allowPlayerDraw = true 48 | cam.Start3D() 49 | cam.End3D() 50 | local ogEyePos = EyePos 51 | EyePos = function() return Vector(0, 0, 0) end 52 | local ogRenderOverride = LocalPlayer().RenderOverride 53 | LocalPlayer().RenderOverride = nil 54 | render.SuppressEngineLighting(true) 55 | LocalPlayer():DrawModel() 56 | render.SuppressEngineLighting(false) 57 | EyePos = ogEyePos 58 | LocalPlayer().RenderOverride = ogRenderOverride 59 | g_VR.allowPlayerDraw = allowOrig 60 | cam.Start3D() 61 | cam.End3D() 62 | render.CullMode(0) 63 | render.PopRenderTarget() 64 | cam.End3D() 65 | render.SetMaterial(mat_mirror) 66 | render.DrawQuadEasy(mirrorPos, mirrorAng:Up(), 30, 60, color_white, 0) 67 | end) 68 | end 69 | 70 | function VRUtilOpenHeightMenu() 71 | if not g_VR.threePoints or VRUtilIsMenuOpen("heightmenu") then return end 72 | RenderMirror() 73 | VRUtilMenuOpen("heightmenu", 300, 512, nil, nil, Vector(), Angle(), 0.1, true, function() 74 | hook.Remove("PreDrawTranslucentRenderables", "vrmodheightmirror") 75 | hook.Remove("VRMod_Input", "vrmodheightmenuinput") 76 | end) 77 | 78 | local buttons, renderControls 79 | buttons = { 80 | { 81 | x = 250, 82 | y = 0, 83 | w = 50, 84 | h = 50, 85 | text = "X", 86 | font = "Trebuchet24", 87 | text_x = 25, 88 | text_y = 15, 89 | enabled = true, 90 | fn = function() 91 | VRUtilMenuClose("heightmenu") 92 | convars.vrmod_heightmenu:SetBool(false) 93 | end 94 | }, 95 | { 96 | x = 250, 97 | y = 200, 98 | w = 50, 99 | h = 50, 100 | text = "+", 101 | font = "Trebuchet24", 102 | text_x = 25, 103 | text_y = 15, 104 | enabled = not convarValues.vrmod_seated, 105 | fn = function() 106 | g_VR.scale = g_VR.scale + 0.5 107 | convars.vrmod_scale:SetFloat(g_VR.scale) 108 | end 109 | }, 110 | { 111 | x = 250, 112 | y = 255, 113 | w = 50, 114 | h = 50, 115 | text = "Auto\nScale", 116 | font = "Trebuchet24", 117 | text_x = 25, 118 | text_y = 0, 119 | enabled = not convarValues.vrmod_seated, 120 | fn = function() AutoScale() end 121 | }, 122 | { 123 | x = 250, 124 | y = 310, 125 | w = 50, 126 | h = 50, 127 | text = "-", 128 | font = "Trebuchet24", 129 | text_x = 25, 130 | text_y = 15, 131 | enabled = not convarValues.vrmod_seated, 132 | fn = function() 133 | g_VR.scale = g_VR.scale - 0.5 134 | convars.vrmod_scale:SetFloat(g_VR.scale) 135 | end 136 | }, 137 | { 138 | x = 0, 139 | y = 200, 140 | w = 50, 141 | h = 50, 142 | text = convarValues.vrmod_seated and "Disable\nSeated\nOffset" or "Enable\nSeated\nOffset", 143 | font = "Trebuchet18", 144 | text_x = 25, 145 | text_y = -2, 146 | enabled = true, 147 | fn = function() 148 | local newState = not convarValues.vrmod_seated 149 | convars.vrmod_seated:SetBool(newState) 150 | buttons[5].text = newState and "Disable\nSeated\nOffset" or "Enable\nSeated\nOffset" 151 | buttons[2].enabled = not newState 152 | buttons[3].enabled = not newState 153 | buttons[4].enabled = not newState 154 | buttons[6].enabled = newState 155 | renderControls() 156 | end 157 | }, 158 | { 159 | x = 0, 160 | y = 255, 161 | w = 50, 162 | h = 50, 163 | text = "Auto\nOffset", 164 | font = "Trebuchet18", 165 | text_x = 25, 166 | text_y = 5, 167 | enabled = convarValues.vrmod_seated, 168 | fn = function() 169 | local offset = 66.8 - (g_VR.tracking.hmd.pos.z - convarValues.vrmod_seatedoffset - g_VR.origin.z) 170 | convars.vrmod_seatedoffset:SetFloat(offset) 171 | end 172 | } 173 | } 174 | 175 | renderControls = function() 176 | VRUtilMenuRenderStart("heightmenu") 177 | surface.SetDrawColor(0, 0, 0, 255) 178 | draw.DrawText("note: disable seated mode\nand stand IRL when adjusting scale", "Trebuchet18", 3, -2, color_black, TEXT_ALIGN_LEFT) 179 | for _, btn in ipairs(buttons) do 180 | surface.SetDrawColor(0, 0, 0, btn.enabled and 255 or 128) 181 | surface.DrawRect(btn.x, btn.y, btn.w, btn.h) 182 | draw.DrawText(btn.text, btn.font, btn.x + btn.text_x, btn.y + btn.text_y, color_white, TEXT_ALIGN_CENTER) 183 | end 184 | 185 | VRUtilMenuRenderEnd() 186 | end 187 | 188 | renderControls() 189 | hook.Add("VRMod_Input", "vrmodheightmenuinput", function(action, pressed) 190 | if g_VR.menuFocus == "heightmenu" and action == "boolean_primaryfire" and pressed then 191 | for _, btn in ipairs(buttons) do 192 | if btn.enabled and g_VR.menuCursorX > btn.x and g_VR.menuCursorX < btn.x + btn.w and g_VR.menuCursorY > btn.y and g_VR.menuCursorY < btn.y + btn.h then btn.fn() end 193 | end 194 | end 195 | end) 196 | end 197 | 198 | hook.Add("VRMod_Start", "vrmod_OpenHeightMenuOnStartup", function(ply) 199 | if ply == LocalPlayer() and convars.vrmod_heightmenu:GetBool() then 200 | timer.Create("vrmod_HeightMenuStartupWait", 1, 0, function() 201 | if g_VR.threePoints then 202 | timer.Remove("vrmod_HeightMenuStartupWait") 203 | VRUtilOpenHeightMenu() 204 | end 205 | end) 206 | end 207 | end) -------------------------------------------------------------------------------- /lua/vrmod/ui/cl_mapbrowser.lua: -------------------------------------------------------------------------------- 1 | if SERVER then return end 2 | local window = nil 3 | function VRUtilCreateMapBrowserWindow() 4 | if IsValid(window) then return window end 5 | window = vgui.Create("DFrame") 6 | window:SetPos(0, 0) 7 | window:SetSize(915, 512) 8 | window:SetTitle("VRMod Map Browser") 9 | window:MakePopup() 10 | function window:Paint(w, h) 11 | surface.SetDrawColor(255, 255, 255, 255) 12 | surface.DrawRect(0, 0, w, h) 13 | surface.SetDrawColor(80, 80, 80, 255) 14 | surface.DrawRect(0, 0, w, 28) 15 | end 16 | 17 | --########################## pre processing ############################ 18 | local sortedMaps = {} 19 | local mapCategories = { 20 | ["Age of Chivalry"] = {"aoc_"}, 21 | ["INFRA"] = {"infra_"}, 22 | ["Alien Swarm"] = {"^asi-", "lobby"}, 23 | ["Blade Symphony"] = {"cp_docks", "cp_parkour", "cp_sequence", "cp_terrace", "cp_test", "duel_", "ffa_community", "free_", "practice_box", "tut_training", "lightstyle_test"}, 24 | ["Counter-Strike"] = {"ar_", "cs_", "de_", "es_", "fy_", "gd_", "dz_", "training1"}, 25 | ["Day Of Defeat"] = {"dod_"}, 26 | ["Dino D-Day"] = {"ddd_"}, 27 | ["DIPRIP"] = {"de_dam", "dm_city", "dm_refinery", "dm_supermarket", "dm_village", "ur_city", "ur_refinery", "ur_supermarket", "ur_village"}, 28 | ["Dystopia"] = {"dys_", "pb_dojo", "pb_rooftop", "pb_round", "pb_urbandome", "sav_dojo6", "varena"}, 29 | ["Half-Life 2"] = {"d1_", "d2_", "d3_"}, 30 | ["Half-Life 2: Deathmatch"] = {"dm_", "halls3"}, 31 | ["Half-Life 2: Episode 1"] = {"ep1_"}, 32 | ["Half-Life 2: Episode 2"] = {"ep2_"}, 33 | ["Half-Life 2: Episode 3"] = {"ep3_"}, 34 | ["Half-Life 2: Lost Coast"] = {"d2_lostcoast"}, 35 | ["Half-Life"] = {"^c[%d]a", "^t0a"}, 36 | ["Half-Life Deathmatch"] = {"boot_camp", "bounce", "crossfire", "datacore", "frenzy", "lambda_bunker", "rapidcore", "snarkpit", "stalkyard", "subtransit", "undertow"}, 37 | ["Insurgency"] = {"ins_"}, 38 | ["Left 4 Dead"] = {"l4d_"}, 39 | ["Left 4 Dead 2"] = {"^c[%d]m", "^c1[%d]m", "curling_stadium", "tutorial_standards", "tutorial_standards_vs"}, 40 | ["Nuclear Dawn"] = {"clocktower", "coast", "downtown", "gate", "hydro", "metro", "metro_training", "oasis", "oilfield", "silo", "sk_metro", "training"}, 41 | ["Pirates, Vikings, & Knights II"] = {"bt_", "lts_", "te_", "tw_"}, 42 | ["Portal"] = {"escape_", "testchmb_"}, 43 | ["Portal 2"] = {"e1912", "^mp_coop_", "^sp_a"}, 44 | ["Team Fortress 2"] = {"achievement_", "arena_", "cp_", "ctf_", "itemtest", "koth_", "mvm_", "pl_", "plr_", "rd_", "pd_", "sd_", "tc_", "tr_", "trade_", "pass_"}, 45 | ["Zombie Panic! Source"] = {"zpa_", "zpl_", "zpo_", "zps_", "zph_"}, 46 | ["Bunny Hop"] = {"bhop_"}, 47 | ["Cinema"] = {"cinema_", "theater_"}, 48 | ["Climb"] = {"xc_"}, 49 | ["Deathrun"] = {"deathrun_", "dr_"}, 50 | ["Flood"] = {"fm_"}, 51 | ["GMod Tower"] = {"gmt_"}, 52 | ["Gun Game"] = {"gg_", "scoutzknivez"}, 53 | ["Jailbreak"] = {"ba_", "jail_", "jb_"}, 54 | ["Minigames"] = {"mg_"}, 55 | ["Pirate Ship Wars"] = {"pw_"}, 56 | ["Prop Hunt"] = {"ph_"}, 57 | ["Roleplay"] = {"rp_"}, 58 | ["Sled Build"] = {"slb_"}, 59 | ["Spacebuild"] = {"sb_"}, 60 | ["Stop it Slender"] = {"slender_"}, 61 | ["Stranded"] = {"gms_"}, 62 | ["Surf"] = {"surf_"}, 63 | ["The Stalker"] = {"ts_"}, 64 | ["Zombie Survival"] = {"zm_", "zombiesurvival_", "zs_"}, 65 | } 66 | 67 | local ignore = {"^background", "^devtest", "^ep1_background", "^ep2_background", "^styleguide", "sdk_", "test_", "vst_", "c4a1y", "credits", "d2_coast_02", "d3_c17_02_camera", "ep1_citadel_00_demo", "intro", "test"} 68 | local gamemodes = engine.GetGamemodes() 69 | for i = 1, #gamemodes do 70 | mapCategories[gamemodes[i].title] = mapCategories[gamemodes[i].title] or {} 71 | local patterns = string.Split(gamemodes[i].maps, "|") 72 | for j = 1, #patterns do 73 | if patterns[j] == "" then continue end 74 | patterns[j] = string.lower(patterns[j]) 75 | local found = false 76 | for k = 1, #mapCategories[gamemodes[i].title] do 77 | if mapCategories[gamemodes[i].title][k] == patterns[j] then 78 | found = true 79 | break 80 | end 81 | end 82 | 83 | if not found then mapCategories[gamemodes[i].title][#mapCategories[gamemodes[i].title] + 1] = patterns[j] end 84 | end 85 | end 86 | 87 | local files, _ = file.Find("maps/*", "GAME") 88 | for i = 1, #files do 89 | if not string.find(files[i], ".bsp$") then continue end 90 | local cont = false 91 | for j = 1, #ignore do 92 | if string.find(files[i], ignore[j]) then 93 | cont = true 94 | break 95 | end 96 | end 97 | 98 | if cont then continue end 99 | local category = "Other" 100 | for k, v in pairs(mapCategories) do 101 | for j = 1, #v do 102 | if string.find(files[i], v[j]) then 103 | category = k 104 | break 105 | end 106 | end 107 | end 108 | 109 | local index = nil 110 | for j = 1, #sortedMaps do 111 | if sortedMaps[j].category == category then 112 | index = j 113 | break 114 | end 115 | end 116 | 117 | if not index then 118 | index = #sortedMaps + 1 119 | sortedMaps[index] = { 120 | ["category"] = category 121 | } 122 | end 123 | 124 | sortedMaps[index][#sortedMaps[index] + 1] = { 125 | ["filename"] = files[i], 126 | ["name"] = string.sub(files[i], 1, #files[i] - 4), 127 | ["icon"] = Material("maps/thumb/" .. string.sub(files[i], 1, #files[i] - 4) .. ".png") 128 | } 129 | 130 | if sortedMaps[index][#sortedMaps[index]].icon:IsError() then sortedMaps[index][#sortedMaps[index]].icon = Material("materials/gui/noicon.png") end 131 | end 132 | 133 | local selectedCategory = 1 134 | local categoryLists = {} 135 | local selectedMap = sortedMaps[1][1] 136 | --########################## left side ############################ 137 | local DPanel = vgui.Create("DPanel", window) 138 | DPanel:SetSize(200, 0) 139 | DPanel:DockMargin(0, 10, 0, 0) 140 | DPanel:Dock(LEFT) 141 | DPanel:SetPaintBackground(false) 142 | local DScrollPanel = vgui.Create("DScrollPanel", DPanel) 143 | DScrollPanel:SetSize(200, 0) 144 | DScrollPanel:Dock(FILL) 145 | for i = 1, #sortedMaps do 146 | local DButton = DScrollPanel:Add("DButton") 147 | DButton:SetText("") 148 | DButton:Dock(TOP) 149 | DButton:SetSize(0, 25) 150 | DButton:DockMargin(0, 0, 0, 5) 151 | function DButton:Paint(w, h) 152 | if selectedCategory == i then 153 | surface.SetDrawColor(153, 204, 255, 255) 154 | else 155 | surface.SetDrawColor(221, 221, 221, 255) 156 | end 157 | 158 | surface.DrawRect(0, 0, w, h) 159 | draw.SimpleText(sortedMaps[i].category, "Trebuchet18", 5, 5, Color(85, 85, 85, 255), TEXT_ALIGN_LEFT, TEXT_ALIGN_TOP) 160 | end 161 | 162 | function DButton:DoClick() 163 | categoryLists[selectedCategory]:SetVisible(false) 164 | selectedCategory = i 165 | categoryLists[selectedCategory]:SetVisible(true) 166 | end 167 | end 168 | 169 | local DButton = vgui.Create("DButton", DPanel) 170 | DButton:SetText("") 171 | DButton:Dock(BOTTOM) 172 | DButton:SetSize(25, 40) 173 | function DButton:DoClick() 174 | RunConsoleCommand("vrmod_autostart", "1") 175 | RunConsoleCommand("changelevel", selectedMap.name) 176 | end 177 | 178 | function DButton:Paint(w, h) 179 | surface.SetDrawColor(0, 108, 204, 255) 180 | surface.DrawRect(0, 0, w, h) 181 | draw.SimpleText("Start Game", "Trebuchet24", w / 2, h / 2, Color(255, 255, 255, 255), TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER) 182 | end 183 | 184 | --########################## right side ############################ 185 | for i = 1, #sortedMaps do 186 | local DScrollPanel = vgui.Create("DScrollPanel", window) 187 | DScrollPanel:DockMargin(10, 10, 0, 0) 188 | DScrollPanel:Dock(FILL) 189 | DScrollPanel:SetVisible(i == 1 and true or false) 190 | categoryLists[#categoryLists + 1] = DScrollPanel 191 | local List = vgui.Create("DIconLayout", DScrollPanel) 192 | List:Dock(FILL) 193 | List:SetSpaceY(5) 194 | List:SetSpaceX(5) 195 | for j = 1, #sortedMaps[i] do 196 | local ListItem = List:Add("DButton") 197 | ListItem:SetSize(130, 145) 198 | ListItem:SetText("") 199 | ListItem.DoClick = function() selectedMap = sortedMaps[i][j] end 200 | function ListItem:Paint(w, h) 201 | if selectedMap == sortedMaps[i][j] then 202 | surface.SetDrawColor(151, 197, 255, 255) 203 | surface.DrawRect(0, 0, w, h) 204 | end 205 | 206 | surface.SetDrawColor(255, 255, 255, 255) 207 | surface.SetMaterial(sortedMaps[i][j].icon) 208 | surface.DrawTexturedRect(2, 2, w - 4, w - 4) 209 | draw.SimpleText(sortedMaps[i][j].name, "DermaDefault", w / 2, h - 2, Color(0, 0, 0, 255), TEXT_ALIGN_CENTER, TEXT_ALIGN_BOTTOM) 210 | end 211 | end 212 | end 213 | return window 214 | end -------------------------------------------------------------------------------- /lua/vrmod/input/sh_doors.lua: -------------------------------------------------------------------------------- 1 | if CLIENT then 2 | local definedGrabPoints = {} 3 | function vrmod.AddUseGrabPoint(uid, map, class, model, bodygroup_id, bodygroup_value, bone, pos, ang_left_hand, ang_right_hand) 4 | definedGrabPoints[class] = definedGrabPoints[class] or {} 5 | definedGrabPoints[class][uid] = { 6 | map = map, 7 | model = model, 8 | bodygroup_id = bodygroup_id, 9 | bodygroup_value = bodygroup_value, 10 | bone = bone, 11 | pos = pos, 12 | angles = {ang_left_hand, ang_right_hand} 13 | } 14 | end 15 | 16 | vrmod.AddUseGrabPoint("default1", nil, "prop_door_rotating", "models/props_c17/door01_left.mdl", 1, 1, 1, Vector(-3, 1, -9), Angle(-90, 0, 0), Angle(-90, 0, 180)) 17 | vrmod.AddUseGrabPoint("default2", nil, "prop_door_rotating", "models/props_c17/door01_left.mdl", 1, 1, 1, Vector(-3, 1, 7), Angle(90, 0, 0), Angle(90, 0, 180)) 18 | vrmod.AddUseGrabPoint("default3", nil, "prop_door_rotating", "models/props_c17/door02_double.mdl", 1, 1, 1, Vector(-3, 1, -9), Angle(-90, 0, 0), Angle(-90, 0, 180)) 19 | vrmod.AddUseGrabPoint("default4", nil, "prop_door_rotating", "models/props_c17/door02_double.mdl", 1, 1, 1, Vector(-3, 1, 7), Angle(90, 0, 0), Angle(90, 0, 180)) 20 | vrmod.AddUseGrabPoint("default5", nil, "prop_dynamic", "models/props_wasteland/exterior_fence003b.mdl", 1, 0, 0, Vector(-8, -22, -25), Angle(0, 0, 0), Angle(0, 0, 0)) 21 | vrmod.AddUseGrabPoint("default6", nil, "prop_dynamic", "models/props_wasteland/exterior_fence003b.mdl", 1, 0, 0, Vector(8, -22, -25), Angle(0, 180, 0), Angle(0, 180, 0)) 22 | vrmod.AddUseGrabPoint("default7", "d1_trainstation_01", "func_door_rotating", "*17", 1, nil, nil, Vector(-3, -13, 3), Angle(), Angle()) --left locker doors 23 | vrmod.AddUseGrabPoint("default8", "d1_trainstation_01", "func_door_rotating", "*23", 1, nil, nil, Vector(-0, -14, 3), Angle(0, 15, 0), Angle(0, 15, 0)) 24 | vrmod.AddUseGrabPoint("default9", "d1_trainstation_01", "func_door_rotating", "*20", 1, nil, nil, Vector(-3, -13, 3), Angle(), Angle()) 25 | vrmod.AddUseGrabPoint("default10", "d1_trainstation_01", "func_door_rotating", "*16", 1, nil, nil, Vector(-3, 12, 3), Angle(), Angle()) --right locker doors 26 | vrmod.AddUseGrabPoint("default11", "d1_trainstation_01", "func_door_rotating", "*15", 1, nil, nil, Vector(-3, 10, 3), Angle(), Angle()) 27 | vrmod.AddUseGrabPoint("default12", "d1_trainstation_01", "func_door_rotating", "*22", 1, nil, nil, Vector(-6, 11, 3), Angle(0, 15, 0), Angle(0, 15, 0)) 28 | vrmod.AddUseGrabPoint("default13", "d1_trainstation_01", "func_door_rotating", "*21", 1, nil, nil, Vector(-6, 9, 3), Angle(0, 15, 0), Angle(0, 15, 0)) 29 | vrmod.AddUseGrabPoint("default14", "d1_trainstation_01", "func_door_rotating", "*19", 1, nil, nil, Vector(-3, 12, 3), Angle(), Angle()) 30 | vrmod.AddUseGrabPoint("default15", "d1_trainstation_01", "func_door_rotating", "*18", 1, nil, nil, Vector(-3, 10, 3), Angle(), Angle()) 31 | local hands = { 32 | { 33 | poseName = "pose_lefthand", 34 | overrideFunc = vrmod.SetLeftHandPose, 35 | getFunc = vrmod.GetLeftHandPose 36 | }, 37 | { 38 | poseName = "pose_righthand", 39 | overrideFunc = vrmod.SetRightHandPose, 40 | getFunc = vrmod.GetRightHandPose, 41 | }, 42 | } 43 | 44 | local actionHand = { 45 | ["boolean_left_pickup"] = hands[1], 46 | ["boolean_right_pickup"] = hands[2] 47 | } 48 | 49 | local function releaseHand(hand) 50 | if hand.grabbing then 51 | hand.grabbing = false 52 | hand.lerpStartTime = SysTime() 53 | hand.lerpStartPos, hand.lerpStartAng = hand.getFunc() 54 | end 55 | end 56 | 57 | local doors = {} 58 | local invalidDoors = {} 59 | hook.Add("OnEntityCreated", "vrmod_doors", function(ent) 60 | local potentialGrabPoints = definedGrabPoints[ent:GetClass()] 61 | if potentialGrabPoints then 62 | timer.Simple(0.1, function() 63 | if not IsValid(ent) then return end 64 | local t = { 65 | ent = ent, 66 | pos = ent:GetPos(), 67 | grabPoints = {} 68 | } 69 | 70 | for k, v in pairs(potentialGrabPoints) do 71 | if (not v.map or game.GetMap() == v.map) and ent:GetModel() == v.model and ent:GetBodygroup(v.bodygroup_id) == v.bodygroup_value then t.grabPoints[#t.grabPoints + 1] = v end 72 | end 73 | 74 | if #t.grabPoints > 0 then 75 | doors[#doors + 1] = t 76 | ent.vrmod_door = true 77 | --print(math.floor(SysTime()),"added door", ent) 78 | ent:CallOnRemove("vrmod_door", function() 79 | for i = 1, #doors do 80 | if doors[i].ent == ent then 81 | table.remove(doors, i) 82 | break 83 | end 84 | end 85 | 86 | hands[1].grabEnt = nil 87 | hands[2].grabEnt = nil 88 | --print("door removed", ent) 89 | end) 90 | else 91 | invalidDoors[#invalidDoors + 1] = ent 92 | end 93 | end) 94 | end 95 | end) 96 | 97 | local function init() 98 | if #doors == 0 then return end 99 | hook.Add("VRMod_AllowDefaultAction", "doors", function(action) if action == "boolean_use" and LocalPlayer():GetEyeTrace().Entity.vrmod_door then return false end end) 100 | hook.Add("VRMod_Input", "doors", function(action, pressed) 101 | local hand = actionHand[action] 102 | if hand then 103 | if pressed then 104 | local handPose = g_VR.tracking[hand.poseName] 105 | local closestEnt, closestGrabPoint, closestDist = nil, nil, 9999 106 | for k, v in ipairs(doors) do 107 | if v.pos:DistToSqr(handPose.pos) < 4096 then 108 | for k2, v2 in ipairs(v.grabPoints) do 109 | local mtx = v2.bone and v.ent:GetBoneMatrix(v2.bone) or v.ent:GetWorldTransformMatrix() 110 | if mtx then 111 | local pos, ang = LocalToWorld(v2.pos, v2.angles[1], mtx:GetTranslation(), mtx:GetAngles()) 112 | local dist = handPose.pos:DistToSqr(pos) 113 | if dist < closestDist and handPose.ang:Forward():Dot(ang:Forward()) > 0.5 then 114 | closestGrabPoint = v2 115 | closestDist = dist 116 | closestEnt = v.ent 117 | end 118 | end 119 | end 120 | end 121 | end 122 | 123 | if closestDist < 64 then 124 | hand.grabbing = true 125 | hand.blockUse = false 126 | hand.grabEnt = closestEnt 127 | hand.grabPoint = closestGrabPoint 128 | hand.lerpStartTime = SysTime() 129 | hand.lerpStartPos, hand.lerpStartAng = hand.getFunc() 130 | hook.Add("VRMod_PreRender", "doors", function() 131 | local remove = true 132 | for k, v in ipairs(hands) do 133 | if v.grabEnt then 134 | local fraction = math.min((SysTime() - v.lerpStartTime) * 10, 1) 135 | local pos, ang 136 | if v.grabbing then 137 | local mtx = v.grabPoint.bone and v.grabEnt:GetBoneMatrix(v.grabPoint.bone) or v.grabEnt:GetWorldTransformMatrix() 138 | pos, ang = LocalToWorld(v.grabPoint.pos, v.grabPoint.angles[k], mtx:GetTranslation(), mtx:GetAngles()) 139 | if fraction == 1 and not v.blockUse then 140 | v.blockUse = true 141 | net.Start("vrmod_doors") 142 | net.WriteEntity(v.grabEnt) 143 | net.SendToServer() 144 | timer.Simple(0.2, function() releaseHand(v) end) 145 | end 146 | else 147 | pos, ang = g_VR.tracking[v.poseName].pos, g_VR.tracking[v.poseName].ang 148 | v.grabEnt = fraction < 1 and v.grabEnt or nil 149 | end 150 | 151 | pos, ang = LerpVector(fraction, v.lerpStartPos, pos), LerpAngle(fraction, v.lerpStartAng, ang) 152 | v.overrideFunc(pos, ang) 153 | remove = remove and fraction == 1 and not v.grabbing 154 | end 155 | end 156 | 157 | if remove then hook.Remove("VRMod_PreRender", "doors") end 158 | end) 159 | return false 160 | end 161 | else 162 | releaseHand(hand) 163 | end 164 | end 165 | end) 166 | end 167 | 168 | local function cleanup() 169 | hook.Remove("VRMod_Input", "doors") 170 | hook.Remove("VRMod_PreRender", "doors") 171 | hook.Remove("VRMod_AllowDefaultAction", "doors") 172 | end 173 | 174 | local _, convarValues = vrmod.AddCallbackedConvar("vrmod_doors", "vrmod_doors", "1", FCVAR_ARCHIVE, "", nil, nil, tobool, function(val) 175 | if val and g_VR.active then 176 | init() 177 | else 178 | cleanup() 179 | end 180 | end) 181 | 182 | hook.Add("VRMod_Start", "doors", function(ply) 183 | if ply ~= LocalPlayer() or not convarValues.vrmod_doors then return end 184 | init() 185 | end) 186 | 187 | hook.Add("VRMod_Exit", "doors", function(ply) 188 | if ply ~= LocalPlayer() then return end 189 | cleanup() 190 | end) 191 | 192 | -- 193 | concommand.Add("vrmod_doordebug", function(ply, cmd, args) 194 | hook[args[1] == "1" and "Add" or "Remove"]("PostDrawTranslucentRenderables", "vrmod_doordebug", function(depth, sky) 195 | if depth or sky then return end 196 | render.SetColorMaterial() 197 | for k, v in ipairs(doors) do 198 | render.DrawWireframeSphere(v.pos, 64, 8, 8) 199 | for k2, v2 in ipairs(v.grabPoints) do 200 | local mtx = v2.bone and v.ent:GetBoneMatrix(v2.bone) or v.ent:GetWorldTransformMatrix() 201 | if mtx then 202 | local pos, ang = LocalToWorld(v2.pos, v2.angles[1], mtx:GetTranslation(), mtx:GetAngles()) 203 | render.DrawWireframeBox(pos, ang, Vector(-1, -1, -1), Vector(1, 1, 1), Color(0, 255, 0, 128)) 204 | end 205 | end 206 | end 207 | 208 | for k, v in ipairs(invalidDoors) do 209 | if IsValid(v) then render.DrawWireframeSphere(v:GetPos(), 64, 8, 8, Color(255, 0, 0)) end 210 | end 211 | end) 212 | end) 213 | --]] 214 | elseif SERVER then 215 | util.AddNetworkString("vrmod_doors") 216 | vrmod.NetReceiveLimited("vrmod_doors", 5, 32, function(len, ply) 217 | local ent = net.ReadEntity() 218 | if hook.Run("PlayerUse", ply, ent) ~= false then ent:Use(ply) end 219 | end) 220 | end -------------------------------------------------------------------------------- /lua/vrmod/ui/cl_ui.lua: -------------------------------------------------------------------------------- 1 | if CLIENT then 2 | g_VR = g_VR or {} 3 | g_VR.menuFocus = false 4 | g_VR.menuCursorX = 0 5 | g_VR.menuCursorY = 0 6 | local heldButtons = { 7 | [MOUSE_LEFT] = false, 8 | [MOUSE_RIGHT] = false, 9 | [MOUSE_MIDDLE] = false 10 | } 11 | 12 | local _, convarValues = vrmod.GetConvars() 13 | local uioutline = CreateClientConVar("vrmod_ui_outline", 0, true, FCVAR_ARCHIVE, nil, 0, 1) 14 | local rt_beam = GetRenderTarget("vrmod_rt_beam", 64, 64, false) 15 | local mat_beam = CreateMaterial("vrmod_mat_beam", "UnlitGeneric", { 16 | ["$basetexture"] = rt_beam:GetName(), 17 | ["$ignorez"] = 1, 18 | ["$vertexcolor"] = 1, 19 | ["$vertexalpha"] = 1 20 | }) 21 | 22 | local function UpdateBeamColor(colorString) 23 | local r, g, b, a = string.match(colorString, "(%d+),(%d+),(%d+),(%d+)") 24 | r, g, b, a = tonumber(r), tonumber(g), tonumber(b), tonumber(a) 25 | if not (r and g and b and a) then return end 26 | mat_beam:SetVector("$color", Vector(r / 255, g / 255, b / 255)) 27 | mat_beam:SetFloat("$alpha", a / 255) 28 | render.PushRenderTarget(rt_beam) 29 | render.Clear(r, g, b, a) 30 | render.PopRenderTarget() 31 | end 32 | 33 | vrmod.AddCallbackedConvar("vrmod_test_ui_testver", nil, 0, nil, "", 0, 1, tonumber) 34 | vrmod.AddCallbackedConvar("vrmod_beam_color", nil, "255,0,0,255", nil, "", nil, nil, nil, function(newValue) UpdateBeamColor(newValue) end) 35 | g_VR.menus = {} 36 | local menus = g_VR.menus 37 | local menuOrder = {} 38 | local menusExist = false 39 | local prevFocusPanel = nil 40 | UpdateBeamColor(convarValues.vrmod_beam_color) 41 | function VRUtilMenuRenderPanel(uid) 42 | if not menus[uid] or not menus[uid].panel or not menus[uid].panel:IsValid() then return end 43 | render.PushRenderTarget(menus[uid].rt) 44 | cam.Start2D() 45 | render.Clear(0, 0, 0, 0, true, true) 46 | local oldclip = DisableClipping(false) 47 | render.SetWriteDepthToDestAlpha(false) 48 | menus[uid].panel:PaintManual() 49 | render.SetWriteDepthToDestAlpha(true) 50 | DisableClipping(oldclip) 51 | cam.End2D() 52 | render.PopRenderTarget() 53 | end 54 | 55 | function VRUtilMenuRenderStart(uid) 56 | render.PushRenderTarget(menus[uid].rt) 57 | cam.Start2D() 58 | render.Clear(0, 0, 0, 0, true, true) 59 | render.SetWriteDepthToDestAlpha(true) 60 | end 61 | 62 | function VRUtilMenuRenderEnd() 63 | cam.End2D() 64 | render.PopRenderTarget() 65 | end 66 | 67 | function VRUtilIsMenuOpen(uid) 68 | return menus[uid] ~= nil 69 | end 70 | 71 | function VRUtilRenderMenuSystem() 72 | if menusExist == false then return end 73 | g_VR.menuFocus = false 74 | local cursorX, cursorY = 0, 0 75 | local menuFocusDist = 99999 76 | local menuFocusPanel = nil 77 | local menuFocusCursorWorldPos = nil 78 | local tms = render.GetToneMappingScaleLinear() 79 | render.SetToneMappingScaleLinear(g_VR.view.dopostprocess and Vector(0.50, 0.50, 0.50) or Vector(1, 1, 1)) 80 | for k, v in ipairs(menuOrder) do 81 | k = v.uid 82 | if v.panel then 83 | if not IsValid(v.panel) or not v.panel:IsVisible() then 84 | VRUtilMenuClose(k) 85 | continue 86 | end 87 | end 88 | 89 | local pos, ang = v.pos, v.ang 90 | if v.uid ~= "heightmenu" then 91 | v.scale = 0.02 92 | if v.attachment then 93 | pos, ang = LocalToWorld(pos, ang, g_VR.tracking.pose_lefthand.pos, g_VR.tracking.pose_lefthand.ang) 94 | else 95 | pos, ang = LocalToWorld(pos, ang, g_VR.origin, g_VR.originAngle) 96 | end 97 | end 98 | 99 | cam.IgnoreZ(true) 100 | cam.Start3D2D(pos, ang, v.scale) 101 | surface.SetDrawColor(255, 255, 255, 255) 102 | surface.SetMaterial(v.mat) 103 | surface.DrawTexturedRect(0, 0, v.width, v.height) 104 | --debug outline 105 | if uioutline:GetBool() then 106 | surface.SetDrawColor(255, 0, 0, 255) 107 | surface.DrawOutlinedRect(0, 0, v.width, v.height) 108 | end 109 | 110 | cam.End3D2D() 111 | cam.IgnoreZ(false) 112 | if v.cursorEnabled then 113 | local cursorWorldPos = Vector(0, 0, 0) 114 | local start = g_VR.tracking.pose_righthand.pos 115 | local dir = g_VR.tracking.pose_righthand.ang:Forward() 116 | local dist = nil 117 | local normal = ang:Up() 118 | local A = normal:Dot(dir) 119 | if A < 0 then 120 | local B = normal:Dot(pos - start) 121 | if B < 0 then 122 | dist = B / A 123 | cursorWorldPos = start + dir * dist 124 | local tp = WorldToLocal(cursorWorldPos, Angle(0, 0, 0), pos, ang) 125 | cursorX = tp.x * 1 / v.scale 126 | cursorY = -tp.y * 1 / v.scale 127 | end 128 | end 129 | 130 | if not cursorX or not cursorY or not v or not v.width or not v.height or not dist or not menuFocusDist then return end 131 | if cursorX > 0 and cursorY > 0 and cursorX < v.width and cursorY < v.height and dist < menuFocusDist then 132 | g_VR.menuFocus = k 133 | menuFocusDist = dist 134 | menuFocusPanel = v.panel 135 | v.lastCursorX = cursorX 136 | v.lastCursorY = cursorY 137 | menuFocusCursorWorldPos = cursorWorldPos 138 | end 139 | end 140 | end 141 | 142 | render.SetToneMappingScaleLinear(tms) 143 | if menuFocusPanel ~= prevFocusPanel then 144 | if IsValid(prevFocusPanel) then prevFocusPanel:SetMouseInputEnabled(false) end 145 | if IsValid(menuFocusPanel) then menuFocusPanel:SetMouseInputEnabled(true) end 146 | gui.EnableScreenClicker(menuFocusPanel ~= nil) 147 | prevFocusPanel = menuFocusPanel 148 | end 149 | 150 | local focus = g_VR.menuFocus 151 | if focus and menus[focus] then 152 | g_VR.menuCursorX = menus[focus].lastCursorX 153 | g_VR.menuCursorY = menus[focus].lastCursorY 154 | render.SetMaterial(mat_beam) 155 | render.DrawBeam(g_VR.tracking.pose_righthand.pos, menuFocusCursorWorldPos, 0.1, 0, 1, Color(255, 255, 255, 255)) 156 | end 157 | 158 | render.DepthRange(0, 1) 159 | end 160 | 161 | function VRUtilMenuOpen(uid, width, height, panel, attachment, pos, ang, scale, cursorEnabled, closeFunc) 162 | VRUtilMenuClose(uid) 163 | menus[uid] = { 164 | uid = uid, 165 | panel = panel, 166 | closeFunc = closeFunc, 167 | attachment = attachment, 168 | pos = pos, 169 | ang = ang, 170 | scale = scale, 171 | cursorEnabled = cursorEnabled, 172 | rt = GetRenderTarget("vrmod_rt_ui_" .. uid, width, height, false), 173 | width = width, 174 | height = height, 175 | lastCursorX = 0, 176 | lastCursorY = 0 177 | } 178 | 179 | menuOrder[#menuOrder + 1] = menus[uid] 180 | local mat = Material("!vrmod_mat_ui_" .. uid) 181 | menus[uid].mat = not mat:IsError() and mat or CreateMaterial("vrmod_mat_ui_" .. uid, "UnlitGeneric", { 182 | ["$basetexture"] = menus[uid].rt:GetName(), 183 | ["$translucent"] = 1 184 | }) 185 | 186 | if panel then 187 | panel:SetPaintedManually(true) 188 | VRUtilMenuRenderPanel(uid) 189 | end 190 | 191 | render.PushRenderTarget(menus[uid].rt) 192 | render.Clear(0, 0, 0, 0) 193 | render.PopRenderTarget() 194 | if GetConVar("vrmod_useworldmodels"):GetBool() then 195 | hook.Add("PostDrawTranslucentRenderables", "vrutil_hook_drawmenus", function(bDrawingDepth, bDrawingSkybox) 196 | if bDrawingSkybox then return end 197 | VRUtilRenderMenuSystem() 198 | end) 199 | end 200 | 201 | menusExist = true 202 | end 203 | 204 | local function SyncCursorToVR() 205 | if not g_VR.menuFocus then return end 206 | local menu = menus[g_VR.menuFocus] 207 | if not menu or not menu.lastCursorX or not menu.lastCursorY then return end 208 | -- Optional: convert local panel coords to screen coords if needed 209 | local x, y = menu.lastCursorX, menu.lastCursorY 210 | input.SetCursorPos(x, y) 211 | -- Also update globals if still needed elsewhere 212 | g_VR.menuCursorX = x 213 | g_VR.menuCursorY = y 214 | end 215 | 216 | function VRUtilMenuClose(uid) 217 | for k, v in pairs(menus) do 218 | if k == uid or not uid then 219 | if IsValid(v.panel) then v.panel:SetPaintedManually(false) end 220 | if v.closeFunc then v.closeFunc() end 221 | for k2, v2 in ipairs(menuOrder) do 222 | if v2 == v then 223 | table.remove(menuOrder, k2) 224 | break 225 | end 226 | end 227 | 228 | menus[k] = nil 229 | end 230 | end 231 | 232 | if table.IsEmpty(menus) then 233 | hook.Remove("PostDrawTranslucentRenderables", "vrutil_hook_drawmenus") 234 | g_VR.menuFocus = false 235 | menusExist = false 236 | gui.EnableScreenClicker(false) 237 | end 238 | end 239 | 240 | hook.Add("VRMod_Input", "ui", function(action, pressed) 241 | if not g_VR.menuFocus then return end 242 | local mouseButton = nil 243 | if action == "boolean_primaryfire" or action == "boolean_car_mouse_left" then 244 | mouseButton = MOUSE_LEFT 245 | elseif action == "boolean_secondaryfire" or action == "boolean_car_mouse_right" then 246 | mouseButton = MOUSE_RIGHT 247 | elseif action == "boolean_sprint" then 248 | mouseButton = MOUSE_MIDDLE 249 | end 250 | 251 | if mouseButton then 252 | heldButtons[mouseButton] = pressed 253 | SyncCursorToVR() 254 | if pressed then 255 | gui.InternalMousePressed(mouseButton) 256 | else 257 | gui.InternalMouseReleased(mouseButton) 258 | end 259 | 260 | VRUtilMenuRenderPanel(g_VR.menuFocus) 261 | end 262 | end) 263 | 264 | hook.Add("Think", "VRUtil_SyncCursorWhileHeld", function() 265 | if not g_VR or not g_VR.menuFocus then return end 266 | SyncCursorToVR() 267 | end) 268 | 269 | local lastMenuFocus = nil 270 | hook.Add("Think", "VRMod_MenuFocusChangeDetect", function() 271 | local cur = g_VR.menuFocus 272 | if cur ~= lastMenuFocus then 273 | -- focus just moved 274 | if cur then 275 | vrmod.logger.Debug("[UI] Now pointing at menu:", cur) 276 | -- sync the OS cursor to the new panel 277 | SyncCursorToVR() 278 | end 279 | 280 | lastMenuFocus = cur 281 | end 282 | end) 283 | end 284 | 285 | concommand.Add("vrmod_vgui_reset", function() 286 | if g_VR and g_VR.menus then 287 | for uid, _ in pairs(g_VR.menus) do 288 | VRUtilMenuClose(uid) 289 | end 290 | end 291 | end) -------------------------------------------------------------------------------- /lua/vrmod/physics/sv_physhands.lua: -------------------------------------------------------------------------------- 1 | if CLIENT then return end 2 | local vrHands = {} 3 | -- Utility to get cached physics data from weapon 4 | local function GetCachedWeaponParams(wep, ply, side) 5 | local radius, reach, mins, maxs, angles = vrmod.utils.GetWeaponMeleeParams(wep, ply, side) 6 | if radius == vrmod.DEFAULT_RADIUS and reach == vrmod.DEFAULT_REACH then return nil end 7 | return radius, reach, mins, maxs, angles 8 | end 9 | 10 | -- Applies sphere collision 11 | local function ApplySphere(hand, handData, radius) 12 | if not IsValid(hand) then return end 13 | hand:PhysicsInitSphere(radius, "metal_bouncy") 14 | local phys = hand:GetPhysicsObject() 15 | if IsValid(phys) then 16 | phys:SetMass(20) 17 | handData.phys = phys 18 | end 19 | end 20 | 21 | -- Applies box collision 22 | local function ApplyBox(hand, handData, mins, maxs, angles) 23 | if not IsValid(hand) then return end 24 | hand:PhysicsInitBox(mins, maxs) 25 | --hand:SetAngles(angles) 26 | local phys = hand:GetPhysicsObject() 27 | if IsValid(phys) then 28 | phys:SetMass(20) 29 | handData.phys = phys 30 | end 31 | end 32 | 33 | -- Apply appropriate collision shape based on weapon 34 | local function UpdateWeaponCollisionShape(ply, wep) 35 | if not IsValid(ply) or not vrmod.IsPlayerInVR(ply) then return end 36 | timer.Simple(0.1, function() 37 | if not IsValid(ply) or not vrmod.IsPlayerInVR(ply) then return end 38 | local hands = vrHands[ply] 39 | if not hands or not hands.right or not IsValid(hands.right.ent) then 40 | vrmod.logger.Debug("UpdateWeaponCollisionShape: No valid right hand for %s", ply:Nick()) 41 | return 42 | end 43 | 44 | local right = hands.right 45 | local hand = right.ent 46 | if not vrmod.utils.IsValidWep(wep) then 47 | vrmod.logger.Debug("UpdateWeaponCollisionShape: Invalid weapon %s, applying default sphere", tostring(wep)) 48 | timer.Simple(0, function() ApplySphere(hand, right, vrmod.DEFAULT_RADIUS) end) 49 | return 50 | end 51 | 52 | local radius, reach, mins, maxs, angles = GetCachedWeaponParams(wep, ply, "right") 53 | if not radius or not mins or not maxs or not angles or radius == vrmod.DEFAULT_RADIUS and reach == vrmod.DEFAULT_REACH and mins == vrmod.DEFAULT_MINS and maxs == vrmod.DEFAULT_MAXS then 54 | vrmod.logger.Debug("UpdateWeaponCollisionShape: Invalid or default params for %s, retrying", wep:GetClass()) 55 | timer.Simple(0.5, function() UpdateWeaponCollisionShape(ply, wep) end) 56 | return 57 | end 58 | 59 | vrmod.logger.Debug("UpdateWeaponCollisionShape: Applying box for %s, mins=%s, maxs=%s", wep:GetClass(), tostring(mins), tostring(maxs)) 60 | timer.Simple(0, function() ApplyBox(hand, right, mins, maxs, angles) end) 61 | end) 62 | end 63 | 64 | -- Triggers on weapon switch 65 | hook.Add("PlayerSwitchWeapon", "VRHand_UpdateCollisionOnWeaponSwitch", function(ply, oldWep, newWep) UpdateWeaponCollisionShape(ply, newWep) end) 66 | local function SpawnVRHands(ply) 67 | if not IsValid(ply) or not ply:Alive() or not vrmod.IsPlayerInVR(ply) then return end 68 | if not vrHands[ply] then vrHands[ply] = {} end 69 | local hands = vrHands[ply] 70 | for _, side in ipairs({"right", "left"}) do 71 | timer.Simple(0, function() 72 | if not IsValid(ply) or not ply:Alive() then return end 73 | local handData = hands[side] 74 | local hand = handData and IsValid(handData.ent) and handData.ent or nil 75 | if not IsValid(hand) then 76 | hand = ents.Create("base_anim") 77 | if not IsValid(hand) then return end 78 | hand:SetPos(ply:GetPos()) 79 | hand:Spawn() 80 | hand:SetPersistent(true) 81 | hand:SetNoDraw(true) 82 | hand:DrawShadow(false) 83 | hand:SetNWBool("isVRHand", true) 84 | -- no model, just physics sphere 85 | local radius = vrmod.DEFAULT_RADIUS or 4 86 | hand:PhysicsInitSphere(radius, "metal_bouncy") 87 | hand:SetCollisionBounds(Vector(-radius, -radius, -radius), Vector(radius, radius, radius)) 88 | hand:SetCollisionGroup(COLLISION_GROUP_WEAPON) 89 | hand:Activate() 90 | local phys = hand:GetPhysicsObject() 91 | if IsValid(phys) then 92 | phys:SetMass(20) 93 | hands[side] = { 94 | ent = hand, 95 | phys = phys 96 | } 97 | else 98 | hand:Remove() 99 | return 100 | end 101 | end 102 | end) 103 | end 104 | 105 | timer.Simple(0.1, function() 106 | if IsValid(ply) and hands.right and IsValid(hands.right.ent) then 107 | local wep = ply:GetActiveWeapon() 108 | if vrmod.utils.IsValidWep(wep) then UpdateWeaponCollisionShape(ply, wep) end 109 | end 110 | end) 111 | end 112 | 113 | local function RemoveVRHands(ply) 114 | if not IsValid(ply) then return end 115 | local hands = vrHands[ply] 116 | if not hands then return end 117 | for _, side in pairs(hands) do 118 | local ent = side.ent 119 | if IsValid(ent) then 120 | ent:SetNoDraw(true) 121 | ent:SetCollisionGroup(COLLISION_GROUP_DEBRIS) 122 | ent:SetPos(Vector(0, 0, -10000)) 123 | if IsValid(side.phys) then 124 | side.phys:EnableMotion(false) 125 | side.phys:Sleep() 126 | end 127 | end 128 | end 129 | end 130 | 131 | hook.Add("PlayerTick", "VRHand_PhysicsSync", function(ply) 132 | 133 | local hands = vrHands[ply] 134 | if (not hands or not hands.right or not hands.left) and not ply:InVehicle() then 135 | RemoveVRHands(ply) 136 | timer.Simple(1, function() SpawnVRHands(ply) end) 137 | return 138 | elseif ply:InVehicle() then 139 | RemoveVRHands(ply) 140 | return 141 | end 142 | 143 | 144 | local function UpdateHand(side, getPos, getAng) 145 | local hand = hands[side] 146 | if not hand or not IsValid(hand.ent) or not IsValid(hand.phys) then return end 147 | local pos = getPos(ply) 148 | local ang = getAng(ply) 149 | -- Apply forward offset 150 | local offsetPos = pos + ang:Forward() * vrmod.DEFAULT_OFFSET 151 | local phys = hand.phys 152 | phys:Wake() 153 | phys:ComputeShadowControl({ 154 | secondstoarrive = engine.TickInterval(), 155 | pos = offsetPos, 156 | angle = ang, 157 | maxangular = 3000, 158 | maxangulardamp = 3000, 159 | maxspeed = 3000, 160 | maxspeeddamp = 300, 161 | dampfactor = 0.3, 162 | teleportdistance = 100, 163 | deltatime = 0 164 | }) 165 | end 166 | 167 | UpdateHand("right", vrmod.GetRightHandPos, vrmod.GetRightHandAng) 168 | UpdateHand("left", vrmod.GetLeftHandPos, vrmod.GetLeftHandAng) 169 | end) 170 | 171 | hook.Add("VRMod_Pickup", "VRHand_BlockPickup", function(ply, ent) 172 | local hands = vrHands[ply] 173 | if hands and (ent == hands.right.ent or ent == hands.left.ent) then return false end 174 | end) 175 | 176 | hook.Add("VRMod_Start", "VRHand_VRStart", function(ply) SpawnVRHands(ply) end) 177 | hook.Add("PlayerSpawn", "VRHand_PlayerSpawn", function(ply) if vrmod.IsPlayerInVR(ply) then timer.Simple(0.1, function() if IsValid(ply) then SpawnVRHands(ply) end end) end end) 178 | hook.Add("PlayerDeath", "VRHand_PlayerDeath", function(ply) RemoveVRHands(ply) end) 179 | hook.Add("VRMod_Exit", "VRHand_VREnd", function(ply) RemoveVRHands(ply) end) 180 | hook.Add("PlayerDisconnected", "VRHand_Disconnect", function(ply) 181 | if vrHands[ply] then 182 | for _, side in pairs(vrHands[ply]) do 183 | if IsValid(side.ent) then side.ent:Remove() end 184 | end 185 | 186 | vrHands[ply] = nil 187 | end 188 | end) 189 | 190 | hook.Add("PreCleanupMap", "VRHand_PreCleanup", function() 191 | for ply, _ in pairs(vrHands) do 192 | RemoveVRHands(ply) 193 | end 194 | end) 195 | 196 | hook.Add("PostCleanupMap", "VRHand_PostCleanup", function() 197 | for _, ply in ipairs(player.GetHumans()) do 198 | if IsValid(ply) and vrmod.IsPlayerInVR(ply) then SpawnVRHands(ply) end 199 | end 200 | end) 201 | 202 | hook.Add("Think", "VRHand_ThinkRespawn", function() 203 | for ply, hands in pairs(vrHands) do 204 | if not IsValid(ply) or not ply:Alive() or not vrmod.IsPlayerInVR(ply) then continue end 205 | local repair = false 206 | for _, side in ipairs({"right", "left"}) do 207 | if not hands[side] or not IsValid(hands[side].ent) then 208 | repair = true 209 | break 210 | end 211 | end 212 | 213 | if repair then SpawnVRHands(ply) end 214 | end 215 | end) 216 | 217 | hook.Add("VRMod_Pickup", "VRHand_AVRMagPickup", function(ply, ent) 218 | if not IsValid(ent) or not string.match(ent:GetClass(), "avrmag_") then return end 219 | local hands = vrHands[ply] 220 | if hands and hands.left and IsValid(hands.left.ent) then hands.left.ent:SetCollisionGroup(COLLISION_GROUP_IN_VEHICLE) end 221 | end) 222 | 223 | hook.Add("VRMod_Drop", "VRHand_AVRMagDrop", function(ply, ent) 224 | if not IsValid(ent) or not string.match(ent:GetClass(), "avrmag_") then return end 225 | local hands = vrHands[ply] 226 | if hands and hands.left and IsValid(hands.left.ent) then hands.left.ent:SetCollisionGroup(COLLISION_GROUP_PASSABLE_DOOR) end 227 | end) -------------------------------------------------------------------------------- /lua/vrmod/input/sh_locomotion.lua: -------------------------------------------------------------------------------- 1 | local _, convarValues = vrmod.GetConvars() 2 | local cv_allowtp = CreateConVar("vrmod_allow_teleport", "1", FCVAR_REPLICATED, "Enable teleportation in VRMod", 0, 1) 3 | local cv_usetp = CreateClientConVar("vrmod_allow_teleport_client", 0, true, FCVAR_ARCHIVE) 4 | local cv_tp_hand = CreateClientConVar("vrmod_teleport_use_left", 0, true, FCVAR_ARCHIVE) 5 | local cv_maxTpDist = CreateConVar("vrmod_teleport_maxdist", 50, FCVAR_ARCHIVE + FCVAR_NOTIFY + FCVAR_REPLICATED, "Maximum teleport distance for VRMod") 6 | if SERVER then 7 | util.AddNetworkString("vrmod_teleport") 8 | vrmod.NetReceiveLimited("vrmod_teleport", 10, 100, function(len, ply) if cv_allowtp:GetBool() and g_VR[ply:SteamID()] ~= nil and (hook.Run("PlayerNoClip", ply, true) == true or ULib and ULib.ucl.query(ply, "ulx noclip") == true) then ply:SetPos(net.ReadVector()) end end) 9 | return 10 | end 11 | 12 | if SERVER then return end 13 | local tpBeamMatrices, tpBeamEnt, tpBeamHitPos = {}, nil, nil 14 | local zeroVec, zeroAng = Vector(), Angle() 15 | local upVec = Vector(0, 0, 1) 16 | -- Vehicle origin tracking 17 | local originVehicleLocalPos, originVehicleLocalAng = Vector(), Angle() 18 | for i = 1, 17 do 19 | tpBeamMatrices[i] = Matrix() 20 | end 21 | 22 | hook.Add("VRMod_Input", "teleport", function(action, pressed) 23 | if action == "boolean_teleport" and not LocalPlayer():InVehicle() and cv_allowtp:GetBool() and cv_usetp:GetBool() then 24 | if pressed then 25 | tpBeamEnt = ClientsideModel("models/vrmod/tpbeam.mdl") 26 | tpBeamEnt:SetRenderMode(RENDERMODE_TRANSCOLOR) 27 | tpBeamEnt.RenderOverride = function(self) 28 | render.SuppressEngineLighting(true) 29 | self:SetupBones() 30 | for i = 1, 17 do 31 | self:SetBoneMatrix(i - 1, tpBeamMatrices[i]) 32 | end 33 | 34 | self:DrawModel() 35 | render.SetColorModulation(1, 1, 1) 36 | render.SuppressEngineLighting(false) 37 | end 38 | 39 | hook.Add("VRMod_PreRender", "teleport", function() 40 | local controllerPos, controllerDir 41 | local cv_maxTpDist = cv_maxTpDist:GetInt() 42 | if cv_tp_hand:GetBool() then 43 | controllerPos, controllerDir = g_VR.tracking.pose_lefthand.pos, g_VR.tracking.pose_lefthand.ang:Forward() 44 | else 45 | controllerPos, controllerDir = g_VR.tracking.pose_righthand.pos, g_VR.tracking.pose_righthand.ang:Forward() 46 | end 47 | 48 | prevPos = controllerPos 49 | local hit = false 50 | for i = 2, 17 do 51 | local d = i - 1 52 | local nextPos = controllerPos + controllerDir * cv_maxTpDist * d + Vector(0, 0, -d * d * 3) 53 | local v = nextPos - prevPos 54 | if not hit then 55 | local tr = util.TraceLine({ 56 | start = prevPos, 57 | endpos = prevPos + v, 58 | filter = function(ent) return ent ~= LocalPlayer() and not ent:GetNWBool("IsVRHand", false) end, 59 | mask = MASK_PLAYERSOLID 60 | }) 61 | 62 | hit = tr.Hit 63 | if hit then 64 | tpBeamMatrices[1] = Matrix() 65 | tpBeamMatrices[1]:Translate(tr.HitPos + tr.HitNormal) 66 | tpBeamMatrices[1]:Rotate(tr.HitNormal:Angle() + Angle(90, 0, 90)) 67 | if tr.HitNormal.z < 0.7 then 68 | tpBeamMatrices[1]:Scale(Vector(0.6, 0.6, 0.6)) 69 | tpBeamEnt:SetColor(Color(255, 0, 0, 200)) 70 | tpBeamHitPos = nil 71 | else 72 | tpBeamEnt:SetColor(Color(7, 255, 0, 200)) 73 | tpBeamHitPos = tr.HitPos 74 | end 75 | 76 | tpBeamEnt:SetPos(tr.HitPos) 77 | end 78 | end 79 | 80 | tpBeamMatrices[i] = Matrix() 81 | tpBeamMatrices[i]:Translate(prevPos + v * 0.5) 82 | tpBeamMatrices[i]:Rotate(v:Angle() + Angle(-90, 0, 0)) 83 | tpBeamMatrices[i]:Scale(Vector(0.5, 0.5, v:Length())) 84 | prevPos = nextPos 85 | end 86 | 87 | if not hit then 88 | tpBeamEnt:SetColor(Color(0, 0, 0, 0)) 89 | tpBeamHitPos = nil 90 | end 91 | end) 92 | else 93 | tpBeamEnt:Remove() 94 | hook.Remove("VRMod_PreRender", "teleport") 95 | if tpBeamHitPos then 96 | net.Start("vrmod_teleport") 97 | net.WriteVector(tpBeamHitPos) 98 | net.SendToServer() 99 | end 100 | end 101 | end 102 | end) 103 | 104 | function VRUtilresetVehicleView() 105 | if not g_VR.threePoints and not LocalPlayer():InVehicle() then return end 106 | originVehicleLocalPos = nil 107 | end 108 | 109 | -- Locomotion start/stop 110 | local function start() 111 | local ply = LocalPlayer() 112 | local followVec = zeroVec 113 | originVehicleLocalPos, originVehicleLocalAng = zeroVec, zeroAng 114 | -- Snap-turn config 115 | local snapTurnAngle = 45 116 | local snapThreshold = 0.5 117 | local snapped = false 118 | -- Dedicated Think hook for snap-turn 119 | hook.Add("Think", "vrmod_snapturn", function() 120 | if not g_VR.threePoints then return end 121 | if convarValues.smoothTurn then 122 | snapped = false 123 | return 124 | end 125 | 126 | local axis = g_VR.input.vector2_smoothturn.x 127 | -- Snap right 128 | if axis > snapThreshold and not snapped then 129 | local pos = ply:GetPos() 130 | g_VR.origin = LocalToWorld(g_VR.origin - pos, zeroAng, pos, Angle(0, -snapTurnAngle, 0)) 131 | g_VR.originAngle.yaw = g_VR.originAngle.yaw - snapTurnAngle 132 | snapped = true 133 | -- Snap left 134 | elseif axis < -snapThreshold and not snapped then 135 | local pos = ply:GetPos() 136 | g_VR.origin = LocalToWorld(g_VR.origin - pos, zeroAng, pos, Angle(0, snapTurnAngle, 0)) 137 | g_VR.originAngle.yaw = g_VR.originAngle.yaw + snapTurnAngle 138 | snapped = true 139 | -- Reset when back in deadzone 140 | elseif math.abs(axis) <= snapThreshold then 141 | snapped = false 142 | end 143 | end) 144 | 145 | -- PreRender hook: smooth-turn and follow logic 146 | hook.Add("PreRender", "vrmod_locomotion", function() 147 | if not g_VR.threePoints then return end 148 | -- Vehicle mode 149 | if ply:InVehicle() then 150 | local v = ply:GetVehicle() 151 | local att = v:GetAttachment(v:LookupAttachment("vehicle_driver_eyes")) 152 | if not originVehicleLocalPos then 153 | local relV, relA = WorldToLocal(g_VR.origin, g_VR.originAngle, g_VR.tracking.hmd.pos, Angle(0, g_VR.tracking.hmd.ang.yaw, 0)) 154 | g_VR.origin, g_VR.originAngle = LocalToWorld(relV + Vector(7, 0, 2), relA, att.Pos, att.Ang) 155 | originVehicleLocalPos, originVehicleLocalAng = WorldToLocal(g_VR.origin, g_VR.originAngle, att.Pos, att.Ang) 156 | end 157 | 158 | g_VR.origin, g_VR.originAngle = LocalToWorld(originVehicleLocalPos, originVehicleLocalAng, att.Pos, att.Ang) 159 | return 160 | end 161 | 162 | -- Exit vehicle 163 | if originVehicleLocalPos then 164 | originVehicleLocalPos = nil 165 | g_VR.originAngle = Angle(0, g_VR.originAngle.yaw, 0) 166 | end 167 | 168 | -- Smooth turn 169 | if convarValues.smoothTurn then 170 | local amt = -g_VR.input.vector2_smoothturn.x * convarValues.smoothTurnRate * RealFrameTime() 171 | if amt ~= 0 then 172 | local pos = ply:GetPos() 173 | g_VR.origin = LocalToWorld(g_VR.origin - pos, zeroAng, pos, Angle(0, amt, 0)) 174 | g_VR.originAngle.yaw = g_VR.originAngle.yaw + amt 175 | end 176 | end 177 | 178 | -- Follow HMD 179 | local pos = ply:GetPos() 180 | local target = g_VR.tracking.hmd.pos + upVec:Cross(g_VR.tracking.hmd.ang:Right()) * -10 181 | followVec = ply:GetMoveType() == MOVETYPE_NOCLIP and zeroVec or Vector((target.x - pos.x) * 8, (pos.y - target.y) * -8, 0) 182 | if followVec:LengthSqr() > 262144 then 183 | g_VR.origin = g_VR.origin + pos - target 184 | g_VR.origin.z = pos.z 185 | return 186 | end 187 | 188 | local ground = ply:GetGroundEntity() 189 | local gvel = IsValid(ground) and ground:GetVelocity() or zeroVec 190 | local vel = ply:GetVelocity() - followVec + gvel 191 | vel.z = 0 192 | if vel:Length() < 15 then vel = zeroVec end 193 | g_VR.origin = g_VR.origin + vel * FrameTime() 194 | g_VR.origin.z = pos.z 195 | end) 196 | 197 | -- CreateMove hook (unchanged) 198 | hook.Add("CreateMove", "vrmod_locomotion", function(cmd) 199 | if not g_VR.threePoints then return end 200 | if ply:InVehicle() then 201 | cmd:SetForwardMove((g_VR.input.vector1_forward - g_VR.input.vector1_reverse) * 400) 202 | local inputVector 203 | if g_VR.wheelGripped then 204 | inputVector = g_VR.analog_input.steer 205 | else 206 | inputVector = g_VR.input.vector2_steer.x 207 | end 208 | 209 | cmd:SetSideMove(inputVector * 400) 210 | local _, ra = WorldToLocal(Vector(), g_VR.tracking.hmd.ang, Vector(), ply:GetVehicle():GetAngles()) 211 | cmd:SetViewAngles(ra) 212 | cmd:SetButtons(bit.bor(cmd:GetButtons(), g_VR.input.boolean_turbo and IN_SPEED or 0, g_VR.input.boolean_handbrake and IN_JUMP or 0)) 213 | return 214 | end 215 | 216 | local mt = ply:GetMoveType() 217 | cmd:SetButtons(bit.bor(cmd:GetButtons(), g_VR.input.boolean_jump and IN_JUMP + IN_DUCK or 0, g_VR.input.boolean_sprint and IN_SPEED or 0, mt == MOVETYPE_LADDER and IN_FORWARD or 0, g_VR.tracking.hmd.pos.z < g_VR.origin.z + convarValues.crouchThreshold and IN_DUCK or 0)) 218 | local va = g_VR.currentvmi and g_VR.currentvmi.wrongMuzzleAng and g_VR.tracking.pose_righthand.ang or g_VR.viewModelMuzzle and g_VR.viewModelMuzzle.Ang or g_VR.tracking.hmd.ang 219 | cmd:SetViewAngles(va:Forward():Angle()) 220 | if mt == MOVETYPE_NOCLIP then 221 | cmd:SetForwardMove(math.abs(g_VR.input.vector2_walkdirection.y) > 0.5 and g_VR.input.vector2_walkdirection.y or 0) 222 | cmd:SetSideMove(math.abs(g_VR.input.vector2_walkdirection.x) > 0.5 and g_VR.input.vector2_walkdirection.x or 0) 223 | return 224 | end 225 | 226 | local jv = LocalToWorld(Vector(g_VR.input.vector2_walkdirection.y * math.abs(g_VR.input.vector2_walkdirection.y), -g_VR.input.vector2_walkdirection.x * math.abs(g_VR.input.vector2_walkdirection.x), 0) * ply:GetMaxSpeed() * 0.9, zeroAng, zeroVec, Angle(0, convarValues.controllerOriented and g_VR.tracking.pose_lefthand.ang.yaw or g_VR.tracking.hmd.ang.yaw, 0)) 227 | local wr = WorldToLocal(followVec + jv, zeroAng, zeroVec, Angle(0, va.yaw, 0)) 228 | cmd:SetForwardMove(wr.x) 229 | cmd:SetSideMove(-wr.y) 230 | end) 231 | end 232 | 233 | local function stop() 234 | hook.Remove("Think", "vrmod_snapturn") 235 | hook.Remove("CreateMove", "vrmod_locomotion") 236 | hook.Remove("PreRender", "vrmod_locomotion") 237 | hook.Remove("VRMod_PreRender", "teleport") 238 | if IsValid(tpBeamEnt) then tpBeamEnt:Remove() end 239 | vrmod.RemoveInGameMenuItem("Map Browser") 240 | vrmod.RemoveInGameMenuItem("Reset Vehicle View") 241 | end 242 | 243 | -- Register locomotion 244 | timer.Simple(0, function() vrmod.AddLocomotionOption("default", start, stop, options) end) -------------------------------------------------------------------------------- /lua/vrmod/utils/sh_vehicles.lua: -------------------------------------------------------------------------------- 1 | g_VR = g_VR or {} 2 | vrmod = vrmod or {} 3 | vrmod.utils = vrmod.utils or {} 4 | local cachedBonePos, cachedBoneAng, cachedFrame = nil, nil, 0 5 | --Vehicles/Glide 6 | local function GetApproximateBoneId(vehicle, targetNames) 7 | if not IsValid(vehicle) then return nil end 8 | local boneCount = vehicle:GetBoneCount() 9 | if boneCount <= 0 then return nil end 10 | for i = 0, boneCount - 1 do 11 | local boneName = vehicle:GetBoneName(i) 12 | if boneName then 13 | for _, target in ipairs(targetNames) do 14 | if string.find(string.lower(boneName), string.lower(target), 1, true) then return i end 15 | end 16 | end 17 | end 18 | return nil 19 | end 20 | 21 | function vrmod.utils.GetVehicleBonePosition(vehicle, boneId) 22 | if not IsValid(vehicle) or not boneId then return nil, nil end 23 | if g_VR.vehicle.glide then return vehicle:GetBonePosition(boneId) end 24 | if cachedFrame == FrameNumber() then return cachedBonePos, cachedBoneAng end 25 | local m = vehicle:GetBoneMatrix(boneId) 26 | if not m then return nil, nil end 27 | cachedBonePos, cachedBoneAng = m:GetTranslation(), m:GetAngles() 28 | cachedFrame = FrameNumber() 29 | return cachedBonePos, cachedBoneAng 30 | end 31 | 32 | function vrmod.utils.GetSteeringInfo(ply) 33 | if not IsValid(ply) or not ply:InVehicle() then return nil, nil, nil, nil end 34 | local glideVeh, vehicle 35 | if ply.GlideGetVehicle then 36 | glideVeh = ply:GlideGetVehicle() 37 | if IsValid(glideVeh) then 38 | vehicle = glideVeh 39 | else 40 | vehicle = ply:GetVehicle() 41 | end 42 | else 43 | vehicle = ply:GetVehicle() 44 | end 45 | 46 | if not IsValid(vehicle) then return nil, nil, nil, nil end 47 | local seatIndex = ply.GlideGetSeatIndex and ply:GlideGetSeatIndex() or 1 48 | local sitSeq = glideVeh and glideVeh.GetPlayerSitSequence and glideVeh:GetPlayerSitSequence(seatIndex) 49 | -- Define bone search priorities 50 | local bonePriority = { 51 | motorcycle = { 52 | names = {"handlebars", "handles", "Airboat.Steer", "handle", "steer", "steerw_bone"}, 53 | type = "motorcycle" 54 | }, 55 | car = { 56 | names = {"steering_wheel", "steering", "Rig_Buggy.Steer_Wheel", "car.steer_wheel", "steer", "steerw_bone"}, 57 | type = "car" 58 | } 59 | } 60 | 61 | -- Decide search order based on Glide type 62 | local searchOrder 63 | if IsValid(glideVeh) then 64 | local vType = glideVeh.VehicleType 65 | if vType == Glide.VEHICLE_TYPE.MOTORCYCLE or sitSeq == "drive_airboat" then 66 | searchOrder = {bonePriority.motorcycle, bonePriority.car} 67 | elseif vType == Glide.VEHICLE_TYPE.CAR then 68 | searchOrder = {bonePriority.car, bonePriority.motorcycle} 69 | else 70 | -- For other Glide types, just try both 71 | searchOrder = {bonePriority.motorcycle, bonePriority.car} 72 | end 73 | else 74 | -- No Glide, default to car-first 75 | searchOrder = {bonePriority.car, bonePriority.motorcycle} 76 | end 77 | 78 | -- Search bones 79 | local candidates = {IsValid(glideVeh) and glideVeh or vehicle} 80 | local boneId, boneType, boneName 81 | for _, candidate in ipairs(candidates) do 82 | for _, entry in ipairs(searchOrder) do 83 | for _, name in ipairs(entry.names) do 84 | local id = candidate:LookupBone(name) 85 | if id then 86 | boneId, boneType, boneName = id, entry.type, name 87 | break 88 | end 89 | end 90 | 91 | if not boneId then 92 | local id, approxName = GetApproximateBoneId(candidate, entry.names) 93 | if id then boneId, boneType, boneName = id, entry.type, approxName end 94 | end 95 | 96 | if boneId then break end 97 | end 98 | 99 | if boneId then break end 100 | end 101 | 102 | -- Glide type still takes precedence in naming 103 | if IsValid(glideVeh) then 104 | local vType = glideVeh.VehicleType 105 | if vType == Glide.VEHICLE_TYPE.MOTORCYCLE or sitSeq == "drive_airboat" then 106 | return glideVeh, boneId, "motorcycle", true, boneName 107 | elseif vType == Glide.VEHICLE_TYPE.BOAT then 108 | return glideVeh, boneId, boneType or "boat", true, boneName 109 | elseif vType == Glide.VEHICLE_TYPE.CAR then 110 | return glideVeh, boneId, "car", true, boneName 111 | elseif vType == Glide.VEHICLE_TYPE.PLANE or vType == Glide.VEHICLE_TYPE.HELICOPTER then 112 | return glideVeh, boneId, "aircraft", true, boneName 113 | elseif vType == Glide.VEHICLE_TYPE.TANK then 114 | return glideVeh, boneId, "tank", true, boneName 115 | end 116 | end 117 | 118 | if boneId then return vehicle, boneId, boneType, false, boneName end 119 | return vehicle, nil, "unknown", false, nil 120 | end 121 | 122 | function vrmod.utils.PatchGlideCamera() 123 | local Camera = Glide.Camera 124 | if not Camera then return end 125 | local Config = Glide.Config 126 | if not Config then vrmod.logger.Warn("[Glide] Warning: Glide.Config not found, using fallback values") end 127 | -- Store original functions 128 | if not Camera._OrigCalcView then Camera._OrigCalcView = Camera.CalcView end 129 | if not Camera._OrigCreateMove then Camera._OrigCreateMove = Camera.CreateMove end 130 | -- Override CalcView 131 | function Camera:CalcView() 132 | if not self.isActive then return end 133 | local vehicle = self.vehicle 134 | if not IsValid(vehicle) then return end 135 | local user = self.user 136 | -- VR guard: ensure vrmod and g_VR.view exist 137 | if vrmod and vrmod.IsPlayerInVR and vrmod.IsPlayerInVR(user) and g_VR and g_VR.view then 138 | local hmdPos = g_VR.view.origin or user:EyePos() 139 | local hmdAng = g_VR.view.angles or user:EyeAngles() 140 | local pivot, offset 141 | local angles = hmdAng 142 | if self.isInFirstPerson then 143 | local localEyePos = vehicle:WorldToLocal(hmdPos) 144 | local localPos = vehicle:GetFirstPersonOffset(self.seatIndex, localEyePos) 145 | pivot = vehicle:LocalToWorld(localPos) 146 | offset = Vector() 147 | else 148 | angles = hmdAng + (vehicle.CameraAngleOffset or Angle(0, 0, 0)) 149 | pivot = vehicle:LocalToWorld((vehicle.CameraCenterOffset or Vector()) + (vehicle.CameraTrailerOffset or Vector()) * (self.trailerDistanceFraction or 0)) 150 | local cfgCamDist = Config and Config.cameraDistance or 100 151 | local cfgCamHeight = Config and Config.cameraHeight or 50 152 | local baseOffset = (vehicle.CameraOffset or Vector(-100, 0, 50)) * Vector(cfgCamDist * (1 + (self.trailerDistanceFraction or 0) * (vehicle.CameraTrailerDistanceMultiplier or 0)), 1, cfgCamHeight) 153 | local endPos = pivot + angles:Forward() * baseOffset[1] + angles:Right() * baseOffset[2] + angles:Up() * baseOffset[3] 154 | local offsetDir = endPos - pivot 155 | local offsetLen = offsetDir:Length() 156 | if offsetLen > 0 then 157 | offsetDir:Normalize() 158 | else 159 | offsetDir = angles:Forward() -- arbitrary fallback 160 | offsetLen = 1 161 | end 162 | 163 | -- Local trace table; do NOT rely on global traceData/traceResult 164 | local tr = util.TraceLine({ 165 | start = pivot, 166 | endpos = endPos + offsetDir * 10, 167 | mask = 16395, -- MASK_SOLID_BRUSHONLY 168 | filter = {vehicle} 169 | }) 170 | 171 | local fraction = self.distanceFraction or 1 172 | if tr.Hit then 173 | fraction = tr.Fraction or fraction 174 | if fraction < (self.distanceFraction or 1) then self.distanceFraction = fraction end 175 | end 176 | 177 | offset = (self.shakeOffset or Vector()) + baseOffset * fraction 178 | end 179 | 180 | self.position = pivot + angles:Forward() * offset[1] + angles:Right() * offset[2] + angles:Up() * offset[3] 181 | -- Aim trace from player's eyes using HMD forward 182 | local trAim = util.TraceLine({ 183 | start = user:EyePos(), 184 | endpos = user:EyePos() + hmdAng:Forward() * 50000, 185 | filter = {user, vehicle} 186 | }) 187 | 188 | self.lastAimPos = trAim.HitPos 189 | self.lastAimEntity = trAim.Entity 190 | local aimDir = trAim.HitPos - user:EyePos() 191 | self.lastAimPosDistanceFromEyes = aimDir:Length() 192 | if self.lastAimPosDistanceFromEyes > 0 then 193 | aimDir:Normalize() 194 | self.lastAimPosAnglesFromEyes = aimDir:Angle() 195 | else 196 | self.lastAimPosAnglesFromEyes = hmdAng 197 | end 198 | 199 | -- Keep player's eye angles synced to HMD for weapon aim 200 | if user.SetEyeAngles then user:SetEyeAngles(hmdAng) end 201 | return { 202 | origin = self.position, 203 | angles = angles + (self.punchAngle or Angle(0, 0, 0)), 204 | fov = self.fov, 205 | drawviewer = not self.isInFirstPerson 206 | } 207 | end 208 | -- Non-VR or fallback: call original 209 | return self:_OrigCalcView() 210 | end 211 | 212 | function Camera:CreateMove(cmd) 213 | if self.isActive and vrmod and vrmod.IsPlayerInVR and vrmod.IsPlayerInVR(self.user) then 214 | -- Prefer the cached eye-relative aim angles; fall back to HMD or stored angles if missing. 215 | local setAng = self.lastAimPosAnglesFromEyes or g_VR and g_VR.view and g_VR.view.angles or self.angles or Angle(0, 0, 0) 216 | cmd:SetViewAngles(setAng) 217 | cmd:SetUpMove(math.Clamp(self.lastAimPosDistanceFromEyes or 0, 0, 10000)) 218 | return 219 | end 220 | 221 | -- Non-VR 222 | self:_OrigCreateMove(cmd) 223 | end 224 | end -------------------------------------------------------------------------------- /lua/vrmod/core/sh_startup.lua: -------------------------------------------------------------------------------- 1 | -- Shared VRMod Vehicle Aim Fix with debug prints 2 | local _vrVehicleAimPatched = false 3 | local function PatchVRVehicleAim() 4 | if _vrVehicleAimPatched then return end 5 | _vrVehicleAimPatched = true 6 | local plyMeta = FindMetaTable("Player") 7 | if not plyMeta then return end 8 | local HAND_CORRECTION = Angle(2, 6, 0) -- adjust after testing 9 | local _GetAimVector = plyMeta.GetAimVector 10 | function plyMeta:GetAimVector() 11 | if not self:InVehicle() then return _GetAimVector(self) end 12 | -- Shared VR data access 13 | if g_VR then 14 | if SERVER then 15 | -- Server: VR state is per-player table 16 | local vrData = g_VR[self:SteamID()] 17 | if vrData and vrData.muzzleAng then return vrData.muzzleAng:Forward() end 18 | else 19 | -- Client: VR viewmodel muzzle 20 | if g_VR.viewModelMuzzle and g_VR.viewModelMuzzle.Ang then return g_VR.viewModelMuzzle.Ang:Forward() end 21 | end 22 | end 23 | 24 | -- Fallback to right hand pose 25 | if vrmod and vrmod.GetRightHandPose then 26 | local hand = g_VR and g_VR.tracking and g_VR.tracking.pose_righthand 27 | if hand and hand.Pos and hand.Ang then return (hand.Ang + HAND_CORRECTION):Forward() end 28 | end 29 | -- Fallback to default 30 | return _GetAimVector(self) 31 | end 32 | end 33 | 34 | PatchVRVehicleAim() 35 | if CLIENT then 36 | local convars = vrmod.GetConvars() 37 | vrmod.AddCallbackedConvar("vrmod_configversion", nil, "5") 38 | if convars.vrmod_configversion:GetString() ~= convars.vrmod_configversion:GetDefault() then 39 | timer.Simple(1, function() 40 | for k, v in pairs(convars) do 41 | pcall(function() v:Revert() end) 42 | end 43 | end) 44 | end 45 | 46 | vrmod.AddCallbackedConvar("vrmod_althead", nil, "0") 47 | vrmod.AddCallbackedConvar("vrmod_autostart", nil, "0") 48 | vrmod.AddCallbackedConvar("vrmod_scale", nil, "32.7") 49 | vrmod.AddCallbackedConvar("vrmod_heightmenu", nil, "1") 50 | vrmod.AddCallbackedConvar("vrmod_floatinghands", nil, "0") 51 | vrmod.AddCallbackedConvar("vrmod_desktopview", nil, "3") 52 | vrmod.AddCallbackedConvar("vrmod_useworldmodels", nil, "0") 53 | vrmod.AddCallbackedConvar("vrmod_laserpointer", nil, "0") 54 | vrmod.AddCallbackedConvar("vrmod_znear", nil, "1") 55 | vrmod.AddCallbackedConvar("vrmod_renderoffset", nil, "1") 56 | vrmod.AddCallbackedConvar("vrmod_viewscale", nil, "1.0") 57 | vrmod.AddCallbackedConvar("vrmod_fovscale_x", nil, "1") 58 | vrmod.AddCallbackedConvar("vrmod_fovscale_y", nil, "1") 59 | vrmod.AddCallbackedConvar("vrmod_scalefactor", nil, "1") 60 | vrmod.AddCallbackedConvar("vrmod_eyescale", nil, "0.5") 61 | vrmod.AddCallbackedConvar("vrmod_verticaloffset", nil, "0") 62 | vrmod.AddCallbackedConvar("vrmod_horizontaloffset", nil, "0") 63 | vrmod.AddCallbackedConvar("vrmod_oldcharacteryaw", nil, "0") 64 | vrmod.AddCallbackedConvar("vrmod_postprocess", nil, "0", nil, nil, nil, nil, tobool, function(val) if g_VR.view then g_VR.view.dopostprocess = val end end) 65 | vrmod.AddCallbackedConvar("vrmod_skybox", nil, "0", nil, nil, nil, nil, tobool, function(val) RunConsoleCommand("r_3dsky", val and "1" or "0") end) 66 | vrmod.AddCallbackedConvar("vrmod_controlleroffset_x", nil, "-15") 67 | vrmod.AddCallbackedConvar("vrmod_controlleroffset_y", nil, "-1") 68 | vrmod.AddCallbackedConvar("vrmod_controlleroffset_z", nil, "5") 69 | vrmod.AddCallbackedConvar("vrmod_controlleroffset_pitch", nil, "50") 70 | vrmod.AddCallbackedConvar("vrmod_controlleroffset_yaw", nil, "0") 71 | vrmod.AddCallbackedConvar("vrmod_controlleroffset_roll", nil, "0") 72 | vrmod.AddCallbackedConvar("vrmod_smoothturn", "smoothTurn", "1", nil, nil, nil, nil, tobool) 73 | vrmod.AddCallbackedConvar("vrmod_smoothturnrate", "smoothTurnRate", "180", nil, nil, nil, nil, tonumber) 74 | vrmod.AddCallbackedConvar("vrmod_crouchthreshold", "crouchThreshold", "40", nil, nil, nil, nil, tonumber) 75 | ---------------------------------------------------------------------------- 76 | concommand.Add("vrmod_start", function(ply, cmd, args) 77 | if vgui.CursorVisible() then print("vrmod: attempting startup when game is unpaused") end 78 | timer.Create("vrmod_start", 0.1, 0, function() 79 | if not vgui.CursorVisible() then 80 | timer.Remove("vrmod_start") 81 | VRUtilClientStart() 82 | end 83 | end) 84 | end) 85 | 86 | concommand.Add("vrmod_exit", function(ply, cmd, args) 87 | if timer.Exists("vrmod_start") then timer.Remove("vrmod_start") end 88 | if isfunction(VRUtilClientExit) then VRUtilClientExit() end 89 | end) 90 | 91 | concommand.Add("vrmod_reset", function(ply, cmd, args) 92 | for k, v in pairs(vrmod.GetConvars()) do 93 | pcall(function() v:Revert() end) 94 | end 95 | 96 | hook.Call("VRMod_Reset") 97 | end) 98 | 99 | concommand.Add("vrmod_info", function() 100 | -- simple banner and key–value printer 101 | local function banner() 102 | print(("="):rep(72)) 103 | end 104 | 105 | local function kv(label, val) 106 | print(string.format("| %-30s %s", label, val)) 107 | end 108 | 109 | banner() 110 | -- General info 111 | kv("Addon Version:", vrmod.GetVersion()) 112 | kv("Module Version:", vrmod.GetModuleVersion()) 113 | kv("GMod Version:", VERSION .. " (Branch: " .. BRANCH .. ")") 114 | kv("Operating System:", system.IsWindows() and "Windows" or system.IsLinux() and "Linux" or system.IsOSX() and "OSX" or "Unknown") 115 | kv("Server Type:", game.SinglePlayer() and "Single Player" or "Multiplayer") 116 | kv("Server Name:", GetHostName()) 117 | kv("Server Address:", game.GetIPAddress()) 118 | kv("Gamemode:", GAMEMODE_NAME) 119 | -- Addon counts 120 | local wcount = 0 121 | for _, a in ipairs(engine.GetAddons()) do 122 | if a.mounted then wcount = wcount + 1 end 123 | end 124 | 125 | kv("Workshop Addons:", wcount) 126 | local _, folders = file.Find("addons/*", "GAME") 127 | local blacklist = { 128 | checkers = true, 129 | chess = true, 130 | common = true, 131 | go = true, 132 | hearts = true, 133 | spades = true 134 | } 135 | 136 | local lcount = 0 137 | for _, name in ipairs(folders) do 138 | if not blacklist[name] then lcount = lcount + 1 end 139 | end 140 | 141 | kv("Legacy Addons:", lcount) 142 | print("|" .. ("-"):rep(70)) 143 | -- CRC of data/vrmod and lua/bin 144 | local function dumpCRC(path) 145 | for _, entry in ipairs(file.Find(path .. "/*", "GAME")) do 146 | local full = path .. "/" .. entry 147 | if file.IsDir(full, "GAME") then 148 | dumpCRC(full) 149 | else 150 | local crc = util.CRC(file.Read(full, "GAME") or "") 151 | kv(full, string.format("%X", crc)) 152 | end 153 | end 154 | end 155 | 156 | dumpCRC("data/vrmod") 157 | print("|" .. ("-"):rep(70)) 158 | dumpCRC("lua/bin") 159 | print("|" .. ("-"):rep(70)) 160 | -- Convar list 161 | local names = {} 162 | for _, cv in pairs(convars) do 163 | names[#names + 1] = cv:GetName() 164 | end 165 | 166 | table.sort(names) 167 | for _, n in ipairs(names) do 168 | local cv = GetConVar(n) 169 | local val = cv:GetString() 170 | kv(n, val .. (val ~= cv:GetDefault() and " *" or "")) 171 | end 172 | 173 | banner() 174 | end) 175 | 176 | concommand.Add("vrmod", function(ply, cmd, args) 177 | if vgui.CursorVisible() then print("vrmod: menu will open when game is unpaused") end 178 | timer.Create("vrmod_open_menu", 0.1, 0, function() 179 | if not vgui.CursorVisible() then 180 | VRUtilOpenMenu() 181 | timer.Remove("vrmod_open_menu") 182 | end 183 | end) 184 | end) 185 | elseif SERVER then 186 | -- Mark player on spawn 187 | hook.Add("PlayerSpawn", "VRMarkPlayerForEmptyWeapon", function(ply) if g_VR and g_VR[ply:SteamID()] then ply:SetNWBool("vr_switch_empty", true) end end) 188 | -- Switch weapon in Think hook 189 | hook.Add("Think", "VRSwitchToEmptyWeapon", function() 190 | for _, ply in ipairs(player.GetAll()) do 191 | if ply:GetNWBool("vr_switch_empty") and IsValid(ply) and ply:Alive() then 192 | if ply:HasWeapon("weapon_vrmod_empty") then 193 | ply:SelectWeapon("weapon_vrmod_empty") 194 | ply:SetNWBool("vr_switch_empty", false) 195 | end 196 | end 197 | end 198 | end) 199 | 200 | hook.Add("EntityFireBullets", "VRMod_NoShootOwnVehicle", function(ply, data) 201 | if not ply:IsPlayer() or not ply:InVehicle() then return end 202 | local veh = ply:GetVehicle() 203 | if not IsValid(veh) then return end 204 | -- Walk to top-level vehicle 205 | while IsValid(veh:GetParent()) do 206 | veh = veh:GetParent() 207 | end 208 | 209 | -- Build ignore list (vehicle + welded children) 210 | local ignore = {veh} 211 | for _, c in ipairs(veh:GetChildren()) do 212 | table.insert(ignore, c) 213 | end 214 | 215 | if constraint then 216 | for _, c in ipairs(constraint.GetTable(veh) or {}) do 217 | if IsValid(c.Ent1) then table.insert(ignore, c.Ent1) end 218 | if IsValid(c.Ent2) then table.insert(ignore, c.Ent2) end 219 | end 220 | end 221 | 222 | -- Merge with existing IgnoreEntity 223 | if not data.IgnoreEntity then 224 | data.IgnoreEntity = ignore 225 | elseif type(data.IgnoreEntity) == "Entity" then 226 | data.IgnoreEntity = {data.IgnoreEntity} 227 | for _, e in ipairs(ignore) do 228 | table.insert(data.IgnoreEntity, e) 229 | end 230 | elseif type(data.IgnoreEntity) == "table" then 231 | for _, e in ipairs(ignore) do 232 | table.insert(data.IgnoreEntity, e) 233 | end 234 | end 235 | return true, data 236 | end) 237 | end -------------------------------------------------------------------------------- /lua/vrmod/pickup/sh_pickup.lua: -------------------------------------------------------------------------------- 1 | g_VR = g_VR or {} 2 | vrmod = vrmod or {} 3 | scripted_ents.Register({ 4 | Type = "anim", 5 | Base = "vrmod_pickup" 6 | }, "vrmod_pickup") 7 | 8 | vrmod.AddCallbackedConvar("vrmod_pickup_limit", nil, 1, FCVAR_REPLICATED + FCVAR_NOTIFY + FCVAR_ARCHIVE, "", 0, 3, tonumber) 9 | vrmod.AddCallbackedConvar("vrmod_pickup_range", nil, 3.5, FCVAR_REPLICATED + FCVAR_ARCHIVE, "", 0.0, 999.0, tonumber) 10 | vrmod.AddCallbackedConvar("vrmod_pickup_weight", nil, 150, FCVAR_REPLICATED + FCVAR_ARCHIVE, "", 0, 10000, tonumber) 11 | vrmod.AddCallbackedConvar("vrmod_pickup_npcs", nil, 1, FCVAR_REPLICATED + FCVAR_NOTIFY + FCVAR_ARCHIVE, "", 0, 3, tonumber) 12 | vrmod.AddCallbackedConvar("vrmod_pickup_limit", nil, "1", FCVAR_REPLICATED + FCVAR_NOTIFY + FCVAR_ARCHIVE, "", 0, 3, tonumber) 13 | if CLIENT then 14 | if g_VR then 15 | g_VR.cooldownLeft = false 16 | g_VR.cooldownRight = false 17 | end 18 | 19 | CreateClientConVar("vrmod_pickup_halos", "1", true, FCVAR_CLIENTCMD_CAN_EXECUTE + FCVAR_ARCHIVE) 20 | local pickupTargetEntLeft = nil 21 | local pickupTargetEntRight = nil 22 | local haloTargetsLeft = {} 23 | local haloTargetsRight = {} 24 | -- Cleanup clones only for normal props on drop 25 | hook.Add("VRMod_Drop", "vrmod_drop_cooldown", function(ply, ent) 26 | if not IsValid(ent) or vrmod.utils.IsIgnoredProp(ent) then return end 27 | for _, hand in ipairs({"Left", "Right"}) do 28 | if g_VR then 29 | local key = hand == "Left" and "cooldownLeft" or "cooldownRight" 30 | g_VR[key] = true 31 | timer.Simple(0.5, function() if g_VR then g_VR[key] = false end end) 32 | end 33 | end 34 | end) 35 | 36 | hook.Add("Tick", "vrmod_find_pickup_target", function() 37 | local ply = LocalPlayer() 38 | if not IsValid(ply) or not g_VR or not vrmod.IsPlayerInVR(ply) or not ply:Alive() then return end 39 | local pickupRange = GetConVar("vrmod_pickup_range"):GetFloat() 40 | local heldLeft = g_VR.heldEntityLeft 41 | local heldRight = g_VR.heldEntityRight 42 | local rightHand = g_VR.tracking and g_VR.tracking.pose_righthand 43 | if rightHand and not heldRight and not vrmod.utils.IsValidWep(ply:GetActiveWeapon()) then 44 | pickupTargetEntRight = vrmod.utils.FindPickupTarget(ply, false, rightHand.pos, rightHand.ang, pickupRange) 45 | else 46 | pickupTargetEntRight = nil 47 | end 48 | 49 | local leftHand = g_VR.tracking and g_VR.tracking.pose_lefthand 50 | if leftHand and not heldLeft then 51 | pickupTargetEntLeft = vrmod.utils.FindPickupTarget(ply, true, leftHand.pos, leftHand.ang, pickupRange) 52 | else 53 | pickupTargetEntLeft = nil 54 | end 55 | end) 56 | 57 | hook.Add("PostDrawOpaqueRenderables", "vrmod_draw_pickup_halo", function() 58 | if not GetConVar("vrmod_pickup_halos"):GetBool() then return end 59 | table.Empty(haloTargetsLeft) 60 | table.Empty(haloTargetsRight) 61 | local ply = LocalPlayer() 62 | local heldLeft, heldRight = g_VR.heldEntityLeft, g_VR.heldEntityRight 63 | local holdingRagdoll = IsValid(heldLeft) and heldLeft:GetNWBool("is_npc_ragdoll", false) or IsValid(heldRight) and heldRight:GetNWBool("is_npc_ragdoll", false) 64 | local function ShouldAddHalo(ent) 65 | if not IsValid(ent) or ent == heldLeft or ent == heldRight or holdingRagdoll then return false end 66 | -- Check server flag for pickup validity, fallback to IsValidPickupTarget if flag missing 67 | local serverFlag = ent:GetNWBool("vrmod_pickup_valid_for_" .. ply:SteamID(), nil) 68 | if serverFlag == nil then 69 | -- If no server flag, fallback to your clientside logic 70 | return vrmod.utils.IsValidPickupTarget(ent, ply, false) 71 | end 72 | return serverFlag 73 | end 74 | 75 | if ShouldAddHalo(pickupTargetEntLeft) then haloTargetsLeft[#haloTargetsLeft + 1] = pickupTargetEntLeft end 76 | if ShouldAddHalo(pickupTargetEntRight) then haloTargetsRight[#haloTargetsRight + 1] = pickupTargetEntRight end 77 | if #haloTargetsLeft > 0 then halo.Add(haloTargetsLeft, Color(250, 100, 0), 1, 1, 1, true, true) end 78 | if #haloTargetsRight > 0 then halo.Add(haloTargetsRight, Color(0, 255, 255), 1, 1, 1, true, true) end 79 | end) 80 | 81 | function vrmod.Pickup(bLeftHand, bDrop) 82 | local handStr = bLeftHand and "Left" or "Right" 83 | if bDrop then 84 | local held = g_VR[bLeftHand and "heldEntityLeft" or "heldEntityRight"] 85 | vrmod.logger.Debug("Pickup: Dropping entity from " .. handStr .. " hand -> " .. tostring(held)) 86 | net.Start("vrmod_pickup") 87 | net.WriteBool(bLeftHand) 88 | net.WriteBool(true) 89 | net.SendToServer() 90 | g_VR[bLeftHand and "heldEntityLeft" or "heldEntityRight"] = nil 91 | else 92 | local targetEnt = bLeftHand and pickupTargetEntLeft or pickupTargetEntRight 93 | if IsValid(targetEnt) then 94 | if g_VR[bLeftHand and "heldEntityLeft" or "heldEntityRight"] ~= targetEnt then 95 | vrmod.logger.Debug("Pickup: Attempting to pick up entity with " .. handStr .. " hand -> " .. targetEnt:GetClass() .. " | Model: " .. (targetEnt:GetModel() or "nil")) 96 | net.Start("vrmod_pickup") 97 | net.WriteBool(bLeftHand) 98 | net.WriteBool(false) 99 | net.WriteEntity(targetEnt) 100 | net.SendToServer() 101 | else 102 | vrmod.logger.Debug("Pickup: Already holding target in " .. handStr .. " hand -> " .. targetEnt:GetClass()) 103 | end 104 | else 105 | vrmod.logger.Debug("Pickup: No valid target to pick up in " .. handStr .. " hand") 106 | end 107 | end 108 | end 109 | 110 | net.Receive("vrmod_pickup", function() 111 | local ply, ent = net.ReadEntity(), net.ReadEntity() 112 | if not IsValid(ply) then 113 | vrmod.logger.Debug("net.Receive vrmod_pickup: Invalid player") 114 | return 115 | end 116 | 117 | if not IsValid(ent) then 118 | vrmod.logger.Debug("net.Receive vrmod_pickup: Invalid entity") 119 | return 120 | end 121 | 122 | local bDrop = net.ReadBool() 123 | if bDrop then 124 | vrmod.logger.Debug("net.Receive vrmod_pickup: Player " .. ply:Nick() .. " dropped entity -> " .. ent:GetClass()) 125 | hook.Call("VRMod_Drop", nil, ply, ent) 126 | return 127 | end 128 | 129 | local bLeftHand = net.ReadBool() 130 | local sid = ply:SteamID() 131 | if not g_VR.net[sid] then 132 | vrmod.logger.Debug("net.Receive vrmod_pickup: No VR data for player " .. ply:Nick()) 133 | return 134 | end 135 | 136 | if ply == LocalPlayer() then 137 | g_VR[bLeftHand and "heldEntityLeft" or "heldEntityRight"] = ent 138 | vrmod.logger.Debug("net.Receive vrmod_pickup: Picked up entity with " .. (bLeftHand and "Left" or "Right") .. " hand -> " .. ent:GetClass()) 139 | end 140 | 141 | hook.Call("VRMod_Pickup", nil, ply, ent) 142 | end) 143 | end 144 | 145 | if SERVER then 146 | util.AddNetworkString("vrmod_pickup") 147 | -- Drop function 148 | function vrmod.Drop(steamid, bLeft) 149 | vrmod.logger.Debug("Entering vrmod.Drop with steamid: " .. tostring(steamid) .. ", bLeft: " .. tostring(bLeft)) 150 | local ply = player.GetBySteamID(steamid) 151 | if not IsValid(ply) then vrmod.logger.Debug("vrmod.Drop: invalid player for steamid " .. tostring(steamid)) end 152 | local handVel = Vector(0, 0, 0) 153 | if IsValid(ply) then handVel = bLeft and (vrmod.GetLeftHandVelocity(ply) or Vector(0, 0, 0)) or vrmod.GetRightHandVelocity(ply) or Vector(0, 0, 0) end 154 | vrmod.logger.Debug("vrmod.Drop: hand velocity: " .. tostring(handVel)) 155 | local index, info = vrmod.utils.FindPickupBySteamIDAndHand(steamid, bLeft) 156 | if not index or not info then 157 | vrmod.logger.Debug("vrmod.Drop: no matching pickup entry found for steamid: " .. tostring(steamid)) 158 | return 159 | end 160 | 161 | vrmod.logger.Debug("vrmod.Drop: found pickup entry, releasing...") 162 | vrmod.utils.ReleasePickupEntry(index, info, handVel) 163 | end 164 | 165 | function vrmod.Pickup(ply, bLeftHand, ent) 166 | if not vrmod.utils.ValidatePickup(ply, bLeftHand, ent) then return end 167 | ent = vrmod.utils.HandleNPCRagdoll(ply, ent) 168 | local handPos, handAng = vrmod.utils.GetHandTransform(ply, bLeftHand) 169 | if not handPos or not handAng then return end 170 | if ent:GetClass() == "prop_ragdoll" then 171 | ent.vrmod_physOffsets = vrmod.utils.BuildRagdollOffsets(ent, handPos, handAng) 172 | ent:SetCollisionGroup(COLLISION_GROUP_PASSABLE_DOOR) 173 | else 174 | vrmod.utils.PatchOwnerCollision(ent, ply) 175 | end 176 | 177 | local controller = vrmod.utils.InitPickupController() 178 | local info = vrmod.utils.CreatePickupInfo(ply, bLeftHand, ent, handPos, handAng) 179 | vrmod.utils.AttachPhysicsToController(info, controller) 180 | vrmod.utils.SendPickupNetMessage(ply, ent, bLeftHand) 181 | end 182 | 183 | vrmod.NetReceiveLimited("vrmod_pickup", 10, 400, function(len, ply) 184 | local bLeft = net.ReadBool() 185 | local bDrop = net.ReadBool() 186 | vrmod.logger.Debug("Received net message vrmod_pickup, bLeft: " .. tostring(bLeft) .. ", bDrop: " .. tostring(bDrop) .. ", player: " .. tostring(ply)) 187 | if bDrop then 188 | vrmod.logger.Debug("Calling vrmod.Drop for player: " .. tostring(ply:SteamID()) .. ", bLeft: " .. tostring(bLeft)) 189 | vrmod.Drop(ply:SteamID(), bLeft) 190 | else 191 | local ent = net.ReadEntity() 192 | vrmod.logger.Debug("Calling vrmod.Pickup for entity: " .. tostring(ent)) 193 | vrmod.Pickup(ply, bLeft, ent) 194 | end 195 | end) 196 | 197 | local function UpdatePickupFlags() 198 | vrmod.logger.Debug("Updating pickup flags for all players") 199 | for _, ply in ipairs(player.GetAll()) do 200 | local nearbyEntities = ents.FindInSphere(ply:GetPos(), 300) 201 | local cv = { 202 | vrmod_pickup_npcs = GetConVar("vrmod_pickup_npcs"):GetInt(), 203 | vrmod_pickup_limit = GetConVar("vrmod_pickup_limit"):GetInt(), 204 | vrmod_pickup_weight = GetConVar("vrmod_pickup_weight"):GetFloat() 205 | } 206 | 207 | vrmod.logger.Debug("Convar values: npcs=" .. cv.vrmod_pickup_npcs .. ", limit=" .. cv.vrmod_pickup_limit .. ", weight=" .. cv.vrmod_pickup_weight) 208 | for _, ent in ipairs(nearbyEntities) do 209 | local canPickup = vrmod.utils.CanPickupEntity(ent, ply, cv) 210 | vrmod.logger.Debug("Setting pickup flag for entity: " .. tostring(ent) .. ", player: " .. ply:SteamID() .. ", canPickup: " .. tostring(canPickup)) 211 | ent:SetNWBool("vrmod_pickup_valid_for_" .. ply:SteamID(), canPickup) 212 | end 213 | end 214 | 215 | vrmod.logger.Debug("Finished updating pickup flags") 216 | end 217 | 218 | -- Run every second for performance 219 | timer.Create("VRMod_UpdatePickupFlags", 1, 0, UpdatePickupFlags) 220 | hook.Add("PlayerDeath", "vrmod_drop_items_on_death", function(ply) 221 | if not IsValid(ply) then return end 222 | local sid = ply:SteamID() 223 | -- Force drop for both hands 224 | vrmod.Drop(sid, true) 225 | vrmod.Drop(sid, false) 226 | end) 227 | 228 | hook.Add("AllowPlayerPickup", "vrmod", function(ply) return not g_VR[ply:SteamID()] end) 229 | end -------------------------------------------------------------------------------- /lua/vrmod/input/sh_glide.lua: -------------------------------------------------------------------------------- 1 | if not Glide then return end 2 | g_VR = g_VR or {} 3 | local validVehicleTypes = { 4 | [Glide.VEHICLE_TYPE.CAR] = true, 5 | [Glide.VEHICLE_TYPE.MOTORCYCLE] = true, 6 | [Glide.VEHICLE_TYPE.TANK] = true, 7 | [Glide.VEHICLE_TYPE.BOAT] = true, 8 | [Glide.VEHICLE_TYPE.PLANE] = true, 9 | [Glide.VEHICLE_TYPE.HELICOPTER] = true 10 | } 11 | 12 | if SERVER then 13 | local lastInputTime = {} 14 | local cvar = GetConVar("glide_ragdoll_enable") 15 | if cvar then 16 | cvar:SetInt(0) 17 | timer.Create("ForceGlideRagdollDisable", 30, 0, function() if g_VR.active and cvar:GetInt() ~= 0 then cvar:SetInt(0) end end) 18 | end 19 | 20 | util.AddNetworkString("glide_vr_input") 21 | net.Receive("glide_vr_input", function(_, ply) 22 | if not IsValid(ply) then return end 23 | local vehicle = ply:GetNWEntity("GlideVehicle") 24 | local seatIndex = ply.GlideGetSeatIndex and ply:GlideGetSeatIndex() or 1 25 | if not IsValid(vehicle) or not validVehicleTypes[vehicle.VehicleType] then return end 26 | lastInputTime[ply] = CurTime() 27 | local action = net.ReadString() 28 | if action == "analog" then 29 | -- Read client inputs 30 | local throttle = net.ReadFloat() 31 | local brake = net.ReadFloat() 32 | local steer = net.ReadFloat() 33 | local pitch = net.ReadFloat() 34 | local yaw = net.ReadFloat() 35 | local roll = net.ReadFloat() 36 | -- Lerp factor for smoothing 37 | local lerpFactor = 0.2 38 | -- Helper function: lerp unless new value is 0 39 | local function LerpOrReset(current, new) 40 | if new == 0 then return 0 end 41 | return Lerp(lerpFactor, current, new) 42 | end 43 | 44 | -- Fetch current vehicle inputs 45 | local currentThrottle = vehicle:GetInputFloat(seatIndex, vehicle.VehicleType == Glide.VEHICLE_TYPE.PLANE or vehicle.VehicleType == Glide.VEHICLE_TYPE.HELICOPTER and "throttle" or "accelerate") or 0 46 | local currentBrake = vehicle:GetInputFloat(seatIndex, "brake") or 0 47 | local currentSteer = vehicle:GetInputFloat(seatIndex, "steer") or 0 48 | local currentPitch = vehicle:GetInputFloat(seatIndex, "pitch") or 0 49 | local currentYaw = vehicle:GetInputFloat(seatIndex, "yaw") or 0 50 | local currentRoll = vehicle:GetInputFloat(seatIndex, "roll") or 0 51 | -- Smooth or reset inputs 52 | local newThrottle = LerpOrReset(currentThrottle, throttle) 53 | local newBrake = LerpOrReset(currentBrake, brake) 54 | local newSteer = LerpOrReset(currentSteer, steer) 55 | local newPitch = LerpOrReset(currentPitch, pitch) 56 | local newYaw = LerpOrReset(currentYaw, yaw) 57 | local newRoll = LerpOrReset(currentRoll, roll) 58 | -- Apply to vehicle 59 | vehicle:SetInputFloat(seatIndex, "brake", newBrake) 60 | vehicle:SetInputFloat(seatIndex, "steer", newSteer) 61 | if vehicle.VehicleType == Glide.VEHICLE_TYPE.PLANE or vehicle.VehicleType == Glide.VEHICLE_TYPE.HELICOPTER then 62 | vehicle:SetInputFloat(seatIndex, "throttle", math.Clamp(newThrottle, -1, 1)) 63 | vehicle:SetInputFloat(seatIndex, "pitch", math.Clamp(newPitch, -1, 1)) 64 | vehicle:SetInputFloat(seatIndex, "yaw", math.Clamp(newYaw, -1, 1)) 65 | vehicle:SetInputFloat(seatIndex, "roll", math.Clamp(newRoll, -1, 1)) 66 | else 67 | vehicle:SetInputFloat(seatIndex, "accelerate", newThrottle) 68 | end 69 | 70 | vrmod.logger.Debug(string.format("Server applied - Throttle: %.2f, Brake: %.2f, Steer: %.2f, Pitch: %.2f, Yaw: %.2f, Roll: %.2f", newThrottle, newBrake, newSteer, newPitch, newYaw, newRoll)) 71 | return 72 | end 73 | 74 | local pressed = net.ReadBool() 75 | if action == "boolean_handbrake" then 76 | vehicle:SetInputBool(seatIndex, "handbrake", pressed) 77 | elseif action == "boolean_lights" then 78 | if pressed then 79 | local newState = vehicle:GetHeadlightState() == 0 and 2 or 0 80 | vehicle:ChangeHeadlightState(newState) 81 | end 82 | elseif action == "boolean_horn" then 83 | vehicle:SetInputBool(seatIndex, "horn", pressed) 84 | elseif action == "boolean_shift_up" then 85 | vehicle:SetInputBool(seatIndex, "shift_up", pressed) 86 | elseif action == "boolean_shift_down" then 87 | vehicle:SetInputBool(seatIndex, "shift_down", pressed) 88 | elseif action == "boolean_shift_neutral" then 89 | vehicle:SetInputBool(seatIndex, "shift_neutral", pressed) 90 | elseif action == "boolean_turret" or vehicle.VehicleType == Glide.VEHICLE_TYPE.TANK and action == "boolean_right_pickup" then 91 | vehicle:SetInputBool(seatIndex, "attack", pressed) 92 | elseif action == "boolean_alt_turret" or vehicle.VehicleType == Glide.VEHICLE_TYPE.TANK and action == "boolean_left_pickup" then 93 | vehicle:SetInputBool(seatIndex, "attack_alt", pressed) 94 | elseif action == "boolean_switch_weapon" then 95 | vehicle:SetInputBool(seatIndex, "switch_weapon", pressed) 96 | elseif action == "boolean_siren" then 97 | vehicle:SetInputBool(seatIndex, "siren", pressed) 98 | elseif action == "boolean_signal_left" then 99 | if vehicle.VehicleType == Glide.VEHICLE_TYPE.PLANE or vehicle.VehicleType == Glide.VEHICLE_TYPE.HELICOPTER then 100 | vehicle:SetInputBool(seatIndex, "landing_gear", pressed) 101 | else 102 | vehicle:SetInputBool(seatIndex, "signal_left", pressed) 103 | end 104 | elseif action == "boolean_signal_right" then 105 | if vehicle.VehicleType == Glide.VEHICLE_TYPE.PLANE or vehicle.VehicleType == Glide.VEHICLE_TYPE.HELICOPTER then 106 | vehicle:SetInputBool(seatIndex, "countermeasures", pressed) 107 | else 108 | vehicle:SetInputBool(seatIndex, "signal_right", pressed) 109 | end 110 | elseif action == "boolean_toggle_engine" then 111 | vehicle:SetInputBool(seatIndex, "toggle_engine", pressed) 112 | elseif action == "boolean_switch_weapon" then 113 | vehicle:SetInputBool(seatIndex, "switch_weapon", pressed) 114 | elseif action == "boolen_detach_trailer" then 115 | vehicle:SetInputBool(seatIndex, "detach_trailer", pressed) 116 | end 117 | end) 118 | hook.Add("Think", "GlideVRInputTimeout", function() 119 | local now = CurTime() 120 | for ply, t in pairs(lastInputTime) do 121 | if not IsValid(ply) then 122 | lastInputTime[ply] = nil 123 | continue 124 | end 125 | -- Skip players not in VR 126 | if not vrmod.IsPlayerInVR(ply) then 127 | continue 128 | end 129 | if now - t > 1 then 130 | local vehicle = ply:GetNWEntity("GlideVehicle") 131 | local seatIndex = ply.GlideGetSeatIndex and ply:GlideGetSeatIndex() or 1 132 | if IsValid(vehicle) then 133 | vehicle:SetInputFloat(seatIndex, "throttle", 0) 134 | vehicle:SetInputFloat(seatIndex, "accelerate", 0) 135 | vehicle:SetInputFloat(seatIndex, "brake", 0) 136 | vehicle:SetInputFloat(seatIndex, "steer", 0) 137 | vehicle:SetInputFloat(seatIndex, "pitch", 0) 138 | vehicle:SetInputFloat(seatIndex, "yaw", 0) 139 | vehicle:SetInputFloat(seatIndex, "roll", 0) 140 | end 141 | lastInputTime[ply] = nil 142 | end 143 | end 144 | end) 145 | else -- CLIENT 146 | local originalMouseFlyMode = nil 147 | local originalRagdollEnable = nil 148 | local inputsToSend = { 149 | boolean_handbrake = true, 150 | boolean_lights = true, 151 | boolean_horn = true, 152 | boolean_shift_up = true, 153 | boolean_shift_down = true, 154 | boolean_shift_neutral = true, 155 | boolean_turret = true, 156 | boolean_alt_turret = true, 157 | boolean_switch_weapon = true, 158 | boolean_siren = true, 159 | boolean_signal_left = true, 160 | boolean_signal_right = true, 161 | boolean_toggle_engine = true, 162 | boolen_detach_trailer = true, 163 | boolean_left_pickup = true, 164 | boolean_right_pickup = true, 165 | } 166 | 167 | local lastInputState = {} 168 | local function ApplyMouseFlyMode(mode) 169 | if not Glide or not Glide.Config then return end 170 | local cfg = Glide.Config 171 | cfg.mouseFlyMode = mode 172 | -- Save & sync 173 | if cfg.Save then cfg:Save() end 174 | if cfg.TransmitInputSettings then cfg:TransmitInputSettings(true) end 175 | if SetupFlyMouseModeSettings then SetupFlyMouseModeSettings() end 176 | if Glide.MouseInput and Glide.MouseInput.Activate then Glide.MouseInput:Activate() end 177 | end 178 | 179 | -- Boolean input monitoring 180 | hook.Add("VRMod_Input", "glide_vr_input", function(action, pressed) 181 | if not g_VR.active or not g_VR.input or not g_VR.vehicle.driving or not g_VR.vehicle.current.IsGlideVehicle then return end 182 | local vehicle = g_VR.vehicle.current 183 | if not IsValid(vehicle) or not validVehicleTypes[vehicle.VehicleType] then return end 184 | if not inputsToSend[action] then return end 185 | if lastInputState[action] ~= pressed then 186 | lastInputState[action] = pressed 187 | net.Start("glide_vr_input") 188 | net.WriteString(action) 189 | net.WriteBool(pressed) 190 | net.SendToServer() 191 | end 192 | end) 193 | 194 | hook.Add("VRMod_Start", "Glide_ForceMouseFlyMode", function() 195 | if not (Glide and Glide.Config) then 196 | vrmod.logger.Debug("[Glide] Glide not loaded, skipping mode change") 197 | return 198 | end 199 | 200 | local cfg = Glide.Config 201 | -- Store and disable ragdoll mode for VR 202 | if originalRagdollEnable == nil then 203 | originalRagdollEnable = cfg.glide_ragdoll_enable 204 | if originalRagdollEnable ~= 0 then 205 | vrmod.logger.Debug("[Glide] Disabling Glide ragdoll mode for VR") 206 | cfg.glide_ragdoll_enable = 0 207 | end 208 | end 209 | 210 | -- Store and force mouse fly mode 211 | if cfg.mouseFlyMode ~= 2 then 212 | originalMouseFlyMode = cfg.mouseFlyMode 213 | vrmod.logger.Debug(string.format("[Glide] Saving original mode %s, forcing mode 2", tostring(originalMouseFlyMode))) 214 | ApplyMouseFlyMode(2) 215 | else 216 | vrmod.logger.Debug("[Glide] Mouse fly mode already 2") 217 | end 218 | 219 | if not Glide or not Glide.Camera then return end 220 | vrmod.utils.PatchGlideCamera() 221 | vrmod.logger.Debug("[Glide] Patched Glide.Camera for VR support") 222 | end) 223 | 224 | -- When VR exits 225 | hook.Add("VRMod_Exit", "Glide_RestoreMouseFlyMode", function() 226 | if not (Glide and Glide.Config) then 227 | vrmod.logger.Debug("[Glide] Glide not loaded, cannot restore") 228 | return 229 | end 230 | 231 | local cfg = Glide.Config 232 | -- Restore original mouse fly mode 233 | if originalMouseFlyMode ~= nil then 234 | vrmod.logger.Debug(string.format("[Glide] Restoring original mouse fly mode %s", tostring(originalMouseFlyMode))) 235 | ApplyMouseFlyMode(originalMouseFlyMode) 236 | originalMouseFlyMode = nil 237 | end 238 | 239 | -- Restore original ragdoll mode 240 | if originalRagdollEnable ~= nil then 241 | vrmod.logger.Debug(string.format("[Glide] Restoring original ragdoll mode %s", tostring(originalRagdollEnable))) 242 | cfg.glide_ragdoll_enable = originalRagdollEnable 243 | originalRagdollEnable = nil 244 | end 245 | end) 246 | end --------------------------------------------------------------------------------