diff --git a/shared/make/ropeControl.lua b/shared/make/ropeControl.lua new file mode 100644 index 0000000..29c3f7c --- /dev/null +++ b/shared/make/ropeControl.lua @@ -0,0 +1,255 @@ +if not isServer() then + local RopeCache = { + nextId = 1, + byId = {}, + byHandle = {}, + } + + function loadRopeTextures() + RopeLoadTextures() + local tries = 0 + while not RopeAreTexturesLoaded() and tries < 200 do + Wait(0) + tries += 1 + end + return RopeAreTexturesLoaded() + end + + function maybeUnloadTextures() + local anyLeft = false + local all = GetAllRopes() + if all and type(all) == "table" then + for _, _ in ipairs(all) do anyLeft = true break end + end + if not anyLeft then + RopeUnloadTextures() + end + end + + function registerRope(ropeHandle, startCoords, endCoords, opts) + if not ropeHandle or ropeHandle == 0 then return nil end + local id = RopeCache.nextId + RopeCache.nextId = id + 1 + + local rec = { + handle = ropeHandle, + startCoords = startCoords, + endCoords = endCoords, + type = opts.ropeType, + rate = opts.lengthChangeRate, + breakable = opts.breakable, + createdAt = GetGameTimer() + } + RopeCache.byId[id] = rec + RopeCache.byHandle[ropeHandle] = id + debugPrint("^5Debug^7: 2Registered Rope^7'^3"..id.."^7' - '^3"..ropeHandle.."^7'") + return id + end + + function resolve(id) + if type(id) ~= "number" then return nil, "rope id must be a number" end + local rec = RopeCache.byId[id] + if not rec then return nil, "rope id not found" end + if not DoesRopeExist(rec.handle) then + -- stale; clean mapping + RopeCache.byHandle[rec.handle] = nil + RopeCache.byId[id] = nil + return nil, "rope no longer exists" + end + return rec + end + + --- Create a rope pinned between two coordinates. + --- @param startCoords vector3|table start coord + --- @param endCoords vector3|table end coord + --- @param opts table|nil { ropeType=0..7, breakable=false, collision=true, lockFromFront=false, lengthChangeRate=1.0, timeMultiplier=1.0, slack=0.0, preset=nil } + --- @return integer ropeId + function ropeCreateLine(startCoords, endCoords, opts) + + startCoords = type(startCoords) == "number" and GetEntityCoords(startCoords) or startCoords + endCoords = type(endCoords) == "number" and GetEntityCoords(endCoords) or endCoords + + opts = opts or {} + + local length = #(startCoords - endCoords) + local mid = (startCoords + endCoords) * 0.5 + + local ropeType = opts.ropeType or 1 -- 0..7 (see ropedata.xml types) + local initLength = length + (opts.slack or 0.0) + local maxLength = initLength -- allow droop equal to init + local minLength = 0.0 + local lengthChangeRate = opts.lengthChangeRate or 1.0 + local collisionOn = (opts.collision ~= false) + local lockFromFront = (opts.lockFromFront == true) + local timeMultiplier = opts.timeMultiplier or 1.0 + local breakable = (opts.breakable == true) + + local rope = AddRope(mid.x, mid.y, mid.z, 0.0, 0.0, 0.0, maxLength, ropeType, initLength, minLength, lengthChangeRate, false, collisionOn, lockFromFront, timeMultiplier, breakable, 0) + + if not rope or rope == 0 or not DoesRopeExist(rope) then + debugPrint("^1AddRope failed") + return nil + end + + -- Optional preset (e.g., "ropeFamily3") if provided. + if opts.preset then + -- Best-effort; if invalid it just won't change. + pcall(function() LoadRopeData(rope, tostring(opts.preset)) end) + end + + ActivatePhysics(rope) + + -- Pin both ends to the given coords. + local count = GetRopeVertexCount(rope) + if count and count >= 2 then + PinRopeVertex(rope, 0, startCoords.x, startCoords.y, startCoords.z) + PinRopeVertex(rope, count - 1, endCoords.x, endCoords.y, endCoords.z) + end + + local id = registerRope(rope, startCoords, endCoords, { + ropeType = ropeType, + lengthChangeRate = lengthChangeRate, + breakable = breakable + }) + + return id + end + + --- Delete and unregister a rope. + --- @param id integer + function ropeDelete(id) + local rec, err = resolve(id) + if not rec then return false, err end + local rope = rec.handle + if DoesRopeExist(rope) then + DeleteRope(rope) + end + RopeCache.byHandle[rope] = nil + RopeCache.byId[id] = nil + debugPrint(("deleted rope id=%s"):format(id)) + maybeUnloadTextures() + return true + end + + --- Force rope to a specific length (instant snap). + --- @param id integer + --- @param newLength number + function ropeSetLength(id, newLength) + local rec, err = resolve(id) + if not rec then return false, err end + RopeForceLength(rec.handle, newLength) + return true + end + + --- Smoothly wind the rope (shorten over time). + --- @param id integer + function ropeStartWinding(id) + local rec, err = resolve(id) + if not rec then return false, err end + StartRopeWinding(rec.handle) + return true + end + + --- Stop winding (if winding). + function ropeStopWinding(id) + local rec, err = resolve(id) + if not rec then return false, err end + StopRopeWinding(rec.handle) + return true + end + + --- Smoothly unwind the rope from the front (lengthen over time). + function ropeStartUnwinding(id) + local rec, err = resolve(id) + if not rec then return false, err end + StartRopeUnwindingFront(rec.handle) + return true + end + + --- Stop unwinding (if unwinding). + function ropeStopUnwinding(id) + local rec, err = resolve(id) + if not rec then return false, err end + StopRopeUnwindingFront(rec.handle) + return true + end + + --- Change the wind speed (length change rate). + --- @param id integer + --- @param rate number (units per sec; try 0.5..5.0) + function ropeSetRate(id, rate) + local rec, err = resolve(id) + if not rec then return false, err end + SetRopeLengthChangeRate(rec.handle, rate) + rec.rate = rate + return true + end + + --- Move end endpoint to a new coordinate (re-pins the vertex). + --- @param id integer + --- @param pos vector3|table + function ropeMoveEnd(id, pos) + local rec, err = resolve(id) + if not rec then return false, err end + pos = type(pos) == "number" and GetEntityCoords(pos) or pos + local rope = rec.handle + local count = GetRopeVertexCount(rope) + if not count or count < 2 then return false, "invalid vertex count" end + PinRopeVertex(rope, count - 1, pos.x, pos.y, pos.z) + rec.endCoord = pos + return true + end + + --- Move start endpoint to a new coordinate (re-pins the vertex). + --- @param id integer + --- @param pos vector3|table + function ropeMoveStart(id, pos) + local rec, err = resolve(id) + if not rec then return false, err end + pos = type(pos) == "number" and GetEntityCoords(pos) or pos + local rope = rec.handle + local count = GetRopeVertexCount(rope) + if not count or count < 2 then return false, "invalid vertex count" end + PinRopeVertex(rope, 0, pos.x, pos.y, pos.z) + rec.startCoord = pos + return true + end + + --- Re-pin both endpoints in one call. + function ropeSetEnds(ropeId, startCoord, endCoord) + startCoord = type(startCoord) == "number" and GetEntityCoords(startCoord) or startCoord + endCoord = type(endCoord) == "number" and GetEntityCoords(endCoord) or endCoord + local rec, err = resolve(ropeId) + if not rec then return false, err end + local rope = rec.handle + local count = GetRopeVertexCount(rope) + if not count or count < 2 then + return false, "invalid vertex count" + end + PinRopeVertex(rope, 0, startCoord.x, startCoord.y, startCoord.z) + PinRopeVertex(rope, count - 1, endCoord.x, endCoord.y, endCoord.z) + rec.startCoord = startCoord + rec.endCoord = endCoord + return true + end + + function ropeExists(id) + local rec = RopeCache.byId[id] + return rec and DoesRopeExist(rec.handle) or false + end + + onResourceStart(function() + loadRopeTextures() + end) + + onResourceStop(function() + for id, rec in pairs(RopeCache.byId) do + if rec.handle and DoesRopeExist(rec.handle) then + DeleteRope(rec.handle) + end + RopeCache.byHandle[rec.handle] = nil + RopeCache.byId[id] = nil + end + maybeUnloadTextures() + end) +end \ No newline at end of file diff --git a/starter.lua b/starter.lua index 5a89cdd..1607f15 100644 --- a/starter.lua +++ b/starter.lua @@ -176,6 +176,7 @@ for _, v in pairs({ -- This is a specific load order 'scaleEntity.lua', 'vehicles.lua', 'effects.lua', + 'make/ropeControl.lua', --'warmenu.lua', '_authToken.lua',