├── .gitignore ├── LICENSE ├── ai-components ├── AIControl.lua ├── README.md ├── TargetingHelper.lua └── init.lua ├── mappin-system ├── README.md └── init.lua ├── mod-override ├── ModOverride.lua ├── README.md ├── UI.lua └── init.lua ├── player-actions ├── README.md ├── init.lua └── state.lua ├── settings-system ├── README.md ├── dump.lua └── init.lua ├── stash-anywhere ├── README.md └── init.lua └── vehicle-system ├── README.md └── init.lua /.gitignore: -------------------------------------------------------------------------------- 1 | /.dev 2 | /.idea 3 | /.vs 4 | *.log 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 RED Modding tools 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /ai-components/AIControl.lua: -------------------------------------------------------------------------------- 1 | local TargetingHelper = require('TargetingHelper') 2 | 3 | local AIControl = {} 4 | 5 | local followers = {} 6 | local followTimer = 0.0 7 | local followInterval = 5.0 8 | 9 | local queues = {} 10 | local queueTimer = 0.0 11 | local queueInterval = 0.02 12 | 13 | ---@param targetPosition Vector4 14 | ---@return AIPositionSpec 15 | local function ToPositionSpec(targetPosition) 16 | local worldPosition = WorldPosition.new() 17 | worldPosition:SetVector4(targetPosition) 18 | 19 | local positionSpec = AIPositionSpec.new() 20 | positionSpec:SetWorldPosition(worldPosition) 21 | 22 | return positionSpec 23 | end 24 | 25 | ---@param targetPuppet ScriptedPuppet 26 | ---@param friendPuppet ScriptedPuppet 27 | function AIControl.MakeFriendly(targetPuppet, friendPuppet) 28 | if not friendPuppet then 29 | friendPuppet = Game.GetPlayer() 30 | end 31 | 32 | -- Set NPC attitude to friendly 33 | targetPuppet:GetAttitudeAgent():SetAttitudeGroup(friendPuppet:GetAttitudeAgent():GetAttitudeGroup()) 34 | targetPuppet:GetAttitudeAgent():SetAttitudeTowards(friendPuppet:GetAttitudeAgent(), EAIAttitude.AIA_Friendly) 35 | end 36 | 37 | ---@param targetPuppet ScriptedPuppet 38 | ---@param friendPuppet ScriptedPuppet 39 | function AIControl.MakeNeutral(targetPuppet, friendPuppet) 40 | if not friendPuppet then 41 | friendPuppet = Game.GetPlayer() 42 | end 43 | 44 | -- Restore NPC original group 45 | targetPuppet:GetAttitudeAgent():SetAttitudeGroup(targetPuppet:GetRecord():BaseAttitudeGroup()) 46 | targetPuppet:GetAttitudeAgent():SetAttitudeTowards(friendPuppet:GetAttitudeAgent(), EAIAttitude.AIA_Neutral) 47 | end 48 | 49 | ---@param targetPuppet ScriptedPuppet 50 | ---@param friendPuppet ScriptedPuppet 51 | function AIControl.MakePsycho(targetPuppet, friendPuppet) 52 | if not friendPuppet then 53 | friendPuppet = Game.GetPlayer() 54 | end 55 | 56 | targetPuppet:GetAttitudeAgent():SetAttitudeGroup('HostileToEveryone') 57 | targetPuppet:GetAttitudeAgent():SetAttitudeTowards(friendPuppet:GetAttitudeAgent(), EAIAttitude.AIA_Neutral) 58 | end 59 | 60 | ---@param targetPuppet ScriptedPuppet 61 | ---@return boolean 62 | function AIControl.IsFollower(targetPuppet) 63 | local currentRole = targetPuppet:GetAIControllerComponent():GetAIRole() 64 | 65 | return currentRole and currentRole:IsA('AIFollowerRole') 66 | end 67 | 68 | ---@param targetPuppet ScriptedPuppet 69 | ---@param movementType moveMovementType 70 | ---@return boolean 71 | function AIControl.MakeFollower(targetPuppet, movementType) 72 | if not targetPuppet:IsAttached() then 73 | return false 74 | end 75 | 76 | local currentRole = targetPuppet:GetAIControllerComponent():GetAIRole() 77 | 78 | if currentRole then 79 | if targetPuppet:IsCrowd() and currentRole:IsA('AIFollowerRole') then 80 | return true 81 | end 82 | 83 | currentRole:OnRoleCleared(targetPuppet) 84 | end 85 | 86 | local followerRole = AIFollowerRole.new() 87 | followerRole.followerRef = Game.CreateEntityReference('#player', {}) 88 | 89 | targetPuppet:GetAIControllerComponent():SetAIRole(followerRole) 90 | targetPuppet:GetAIControllerComponent():OnAttach() 91 | 92 | targetPuppet:GetMovePolicesComponent():ChangeMovementType(movementType or moveMovementType.Sprint) 93 | 94 | AIControl.MakeFriendly(targetPuppet) 95 | 96 | for _, followerPuppet in pairs(followers) do 97 | followerPuppet:GetAttitudeAgent():SetAttitudeTowards(targetPuppet:GetAttitudeAgent(), EAIAttitude.AIA_Friendly) 98 | end 99 | 100 | targetPuppet.isPlayerCompanionCachedTimeStamp = 0 101 | 102 | followers[TargetingHelper.GetTargetId(targetPuppet)] = targetPuppet 103 | 104 | return true 105 | end 106 | 107 | ---@param targetPuppet ScriptedPuppet 108 | ---@return boolean 109 | function AIControl.FreeFollower(targetPuppet) 110 | if targetPuppet:IsAttached() then 111 | local currentRole = targetPuppet:GetAIControllerComponent():GetAIRole() 112 | 113 | if currentRole and currentRole:IsA('AIFollowerRole') then 114 | if targetPuppet:IsCrowd() then 115 | targetPuppet:Dispose() -- Can't change roles more than once on crowd npc 116 | else 117 | currentRole:OnRoleCleared(targetPuppet) 118 | 119 | local noRole = AINoRole.new() 120 | 121 | targetPuppet:GetAIControllerComponent():SetAIRole(noRole) 122 | targetPuppet:GetAIControllerComponent():OnAttach() 123 | 124 | AIControl.MakeNeutral(targetPuppet) 125 | 126 | -- Restore sense preset 127 | local sensePreset = targetPuppet:GetRecord():SensePreset():GetID() 128 | SenseComponent.RequestPresetChange(targetPuppet, sensePreset, true) 129 | end 130 | end 131 | end 132 | 133 | followers[TargetingHelper.GetTargetId(targetPuppet)] = nil 134 | 135 | return true 136 | end 137 | 138 | function AIControl.FreeFollowers() 139 | for _, follower in pairs(followers) do 140 | AIControl.FreeFollower(follower) 141 | end 142 | end 143 | 144 | ---@param targetPuppet ScriptedPuppet 145 | function AIControl.InterruptCombat(targetPuppet) 146 | -- Clear threats in case NPC is aggroed 147 | targetPuppet:GetTargetTrackerComponent():ClearThreats() 148 | 149 | -- Reset NPC state to relaxed 150 | NPCPuppet.ChangeHighLevelState(targetPuppet, gamedataNPCHighLevelState.Relaxed) 151 | NPCPuppet.ChangeDefenseModeState(targetPuppet, gamedataDefenseMode.NoDefend) 152 | NPCPuppet.ChangeUpperBodyState(targetPuppet, gamedataNPCUpperBodyState.Normal) 153 | end 154 | 155 | ---@param targetPuppet ScriptedPuppet 156 | ---@param lookAtPuppet ScriptedPuppet 157 | ---@param duration Float|nil 158 | function AIControl.LookAt(targetPuppet, lookAtPuppet, duration) 159 | if not lookAtPuppet then 160 | lookAtPuppet = Game.GetPlayer() 161 | end 162 | 163 | targetPuppet:GetStimReactionComponent():ActivateReactionLookAt(lookAtPuppet, duration and true or false, false, duration, true) 164 | end 165 | 166 | ---@param targetPuppet ScriptedPuppet 167 | function AIControl.StopLookAt(targetPuppet) 168 | targetPuppet:GetStimReactionComponent():DeactiveLookAt(false) 169 | end 170 | 171 | ---@param targetPuppet ScriptedPuppet 172 | ---@param targetPosition Vector4 173 | ---@return AIRotateToCommand 174 | function AIControl.RotateTo(targetPuppet, targetPosition) 175 | local positionSpec = ToPositionSpec(targetPosition) 176 | 177 | local rotateCmd = AIRotateToCommand.new() 178 | rotateCmd.target = positionSpec 179 | rotateCmd.angleTolerance = 5.0 -- If zero then command will never finish 180 | rotateCmd.angleOffset = 0.0 181 | rotateCmd.speed = 1.0 182 | 183 | targetPuppet:GetAIControllerComponent():SendCommand(rotateCmd) 184 | 185 | return rotateCmd, targetPuppet 186 | end 187 | 188 | ---@param targetPuppet ScriptedPuppet 189 | ---@param targetPosition Vector4 190 | ---@param targetRotation Float 191 | ---@return AITeleportCommand 192 | function AIControl.TeleportTo(targetPuppet, targetPosition, targetRotation) 193 | if not targetRotation then 194 | targetRotation = targetPuppet:GetWorldYaw() 195 | end 196 | 197 | local teleportCmd = AITeleportCommand.new() 198 | teleportCmd.position = targetPosition 199 | teleportCmd.rotation = targetRotation 200 | teleportCmd.doNavTest = false 201 | 202 | targetPuppet:GetAIControllerComponent():SendCommand(teleportCmd) 203 | 204 | return teleportCmd, targetPuppet 205 | end 206 | 207 | ---@param targetPuppet ScriptedPuppet 208 | ---@param targetPosition Vector4 209 | ---@param targetDistance Float 210 | ---@param movementType moveMovementType 211 | ---@return AIMoveToCommand 212 | function AIControl.MoveTo(targetPuppet, targetPosition, targetDistance, movementType) 213 | if not targetPosition then 214 | targetPosition = Game.GetPlayer():GetWorldPosition() 215 | end 216 | 217 | if not targetDistance then 218 | targetDistance = 1.0 219 | end 220 | 221 | if not movementType then 222 | movementType = moveMovementType.Run 223 | end 224 | 225 | local positionSpec = ToPositionSpec(targetPosition) 226 | 227 | local moveCmd = AIMoveToCommand.new() 228 | moveCmd.movementTarget = positionSpec 229 | moveCmd.movementType = movementType 230 | moveCmd.desiredDistanceFromTarget = targetDistance 231 | moveCmd.finishWhenDestinationReached = true 232 | moveCmd.ignoreNavigation = true 233 | moveCmd.useStart = true 234 | moveCmd.useStop = false 235 | 236 | targetPuppet:GetAIControllerComponent():SendCommand(moveCmd) 237 | 238 | return moveCmd, targetPuppet 239 | end 240 | 241 | ---@param targetPuppet ScriptedPuppet 242 | ---@param duration Float|nil 243 | ---@return AIHoldPositionCommand 244 | function AIControl.HoldFor(targetPuppet, duration) 245 | local holdCmd = AIHoldPositionCommand.new() 246 | holdCmd.duration = duration or 1.0 247 | holdCmd.ignoreInCombat = false 248 | holdCmd.removeAfterCombat = false 249 | holdCmd.alwaysUseStealth = false 250 | 251 | targetPuppet:GetAIControllerComponent():SendCommand(holdCmd) 252 | 253 | return holdCmd, targetPuppet 254 | end 255 | 256 | ---@param targetPuppet ScriptedPuppet 257 | ---@param followPuppet ScriptedPuppet 258 | ---@param movementType moveMovementType 259 | ---@return AIFollowTargetCommand 260 | function AIControl.FollowTarget(targetPuppet, followPuppet, movementType) 261 | if not followPuppet then 262 | ---@type AIFollowerRole 263 | local currentRole = targetPuppet:GetAIControllerComponent():GetAIRole() 264 | 265 | if currentRole and currentRole:IsA('AIFollowerRole') then 266 | followPuppet = currentRole.followTarget 267 | else 268 | followPuppet = Game.GetPlayer() 269 | end 270 | end 271 | 272 | if not movementType then 273 | movementType = moveMovementType.Sprint 274 | end 275 | 276 | local followCmd = AIFollowTargetCommand.new() 277 | followCmd.target = followPuppet 278 | followCmd.lookAtTarget = followPuppet 279 | followCmd.desiredDistance = 1.0 280 | followCmd.tolerance = 0.5 281 | followCmd.movementType = movementType 282 | followCmd.matchSpeed = true 283 | followCmd.teleport = false 284 | followCmd.stopWhenDestinationReached = false 285 | followCmd.ignoreInCombat = false 286 | followCmd.removeAfterCombat = false 287 | followCmd.alwaysUseStealth = false 288 | 289 | targetPuppet:GetAIControllerComponent():SendCommand(followCmd) 290 | 291 | return followCmd, targetPuppet 292 | end 293 | 294 | ---@param targetPuppet ScriptedPuppet 295 | ---@return AITeleportCommand 296 | function AIControl.InterruptBehavior(targetPuppet) 297 | return AIControl.TeleportTo(targetPuppet, targetPuppet:GetWorldPosition()) 298 | end 299 | 300 | ---@param targetPuppet ScriptedPuppet 301 | ---@param commandInstance AICommand 302 | ---@return boolean 303 | function AIControl.IsCommandActive(targetPuppet, commandInstance) 304 | return AIbehaviorUniqueActiveCommandList.IsActionCommandById( 305 | targetPuppet:GetAIControllerComponent().activeCommands, 306 | commandInstance.id 307 | ) 308 | end 309 | 310 | ---@param targetPuppet ScriptedPuppet 311 | ---@return boolean 312 | function AIControl.HasQueue(targetPuppet) 313 | return queues[TargetingHelper.GetTargetId(targetPuppet)] ~= nil 314 | end 315 | 316 | ---@param targetPuppet ScriptedPuppet 317 | ---@param commandTask function Task function must return a command instance 318 | function AIControl.QueueTask(targetPuppet, commandTask) 319 | local targetId = TargetingHelper.GetTargetId(targetPuppet) 320 | 321 | local queue = queues[targetId] 322 | 323 | if not queue then 324 | queue = { 325 | target = targetPuppet, 326 | tasks = {}, 327 | wait = nil, 328 | } 329 | 330 | queues[targetId] = queue 331 | end 332 | 333 | if not queue.wait then 334 | queue.wait = commandTask() 335 | else 336 | table.insert(queue.tasks, commandTask) 337 | end 338 | end 339 | 340 | ---@param targetPuppet ScriptedPuppet 341 | ---@vararg function 342 | function AIControl.QueueTasks(targetPuppet, ...) 343 | for i = 1, select('#', ...) do 344 | AIControl.QueueTask(targetPuppet, (select(i, ...))) 345 | end 346 | end 347 | 348 | ---@param targetPuppet ScriptedPuppet 349 | function AIControl.ClearQueue(targetPuppet) 350 | local targetId = TargetingHelper.GetTargetId(targetPuppet) 351 | local queue = queues[targetId] 352 | 353 | if queue then 354 | if queue.target:IsAttached() then 355 | queue.target:GetAIControllerComponent():CancelCommand(queue.wait) 356 | queue.target:GetStimReactionComponent():DeactiveLookAt(false) 357 | end 358 | 359 | queues[targetId] = nil 360 | end 361 | end 362 | 363 | function AIControl.ClearQueues() 364 | for targetId, queue in pairs(queues) do 365 | if queue.target:IsAttached() then 366 | queue.target:GetAIControllerComponent():CancelCommand(queue.wait) 367 | queue.target:GetStimReactionComponent():DeactiveLookAt(false) 368 | end 369 | 370 | queues[targetId] = nil 371 | end 372 | end 373 | 374 | ---@param delta number 375 | function AIControl.UpdateTasks(delta) 376 | followTimer = followTimer + delta 377 | 378 | if followTimer >= followInterval then 379 | -- This forces the NPC to follow the player a further outside the NPC's area 380 | for _, follower in pairs(followers) do 381 | if TargetingHelper.IsActive(follower) then 382 | AIControl.FollowTarget(follower) 383 | else 384 | AIControl.FreeFollower(follower) 385 | end 386 | end 387 | 388 | followTimer = followTimer - followInterval 389 | end 390 | 391 | queueTimer = queueTimer + delta 392 | 393 | if queueTimer >= queueInterval then 394 | for key, queue in pairs(queues) do 395 | if not AIControl.IsCommandActive(queue.target, queue.wait) then 396 | repeat 397 | local task = queue.tasks[1] 398 | local command = task() 399 | 400 | table.remove(queue.tasks, 1) 401 | 402 | if command and command:IsA('AICommand') then 403 | queue.wait = command 404 | break 405 | end 406 | until #queue.tasks == 0 407 | 408 | if #queue.tasks == 0 then 409 | queues[key] = nil 410 | end 411 | end 412 | end 413 | 414 | queueTimer = queueTimer - queueInterval 415 | end 416 | end 417 | 418 | function AIControl.Dispose() 419 | AIControl.FreeFollowers() 420 | AIControl.ClearQueues() 421 | end 422 | 423 | return AIControl -------------------------------------------------------------------------------- /ai-components/README.md: -------------------------------------------------------------------------------- 1 | # AI Components Example 2 | 3 | ### Features 4 | 5 | - Mark / unmark the NPC under the crosshair 6 | - Teleport the marked NPCs to selected position (in front of the player) 7 | - Make the marked NPCs run to selected position (in front of the player) 8 | - Turn any or all NPCs in sight into allies who will follow you and fight for you 9 | - Turn all NPCs in sight against each other (affects only NPCs that can fight) 10 | - Queue NPC tasks for consecutive execution 11 | - Remove all markers, clear task queues, and revert recruited NPCs to default state on "Reload All Mods" 12 | 13 | ### References 14 | 15 | - [AI Components](https://codeberg.org/adamsmasher/cyberpunk/src/branch/master/core/components) 16 | - [AI Roles](https://codeberg.org/adamsmasher/cyberpunk/src/branch/master/cyberpunk/ai/roles/aiRole.swift) 17 | - [AI Commands](https://codeberg.org/adamsmasher/cyberpunk/src/branch/master/cyberpunk/ai/commands) 18 | - [Active Command List](https://redscript.redmodding.org/#43287) 19 | - [Attitude Agent](https://redscript.redmodding.org/#27480) 20 | - [Attitude Variants](https://github.com/WolvenKit/CyberCAT/blob/main/CyberCAT.Core/Enums/Dumped%20Enums/EAIAttitude.cs) 21 | - [Targeting System](https://redscript.redmodding.org/#21605) 22 | - [Target Search Queries](https://codeberg.org/adamsmasher/cyberpunk/src/branch/master/core/gameplay/targetingSearchFilter.swift) 23 | - [Mappin System](https://redscript.redmodding.org/#24572) 24 | -------------------------------------------------------------------------------- /ai-components/TargetingHelper.lua: -------------------------------------------------------------------------------- 1 | local TargetingHelper = {} 2 | 3 | local markers = {} 4 | local pins = {} 5 | 6 | ---@param distance number 7 | ---@return Vector4 8 | function TargetingHelper.GetLookAtPosition(distance) 9 | if not distance then 10 | distance = 100 11 | end 12 | 13 | local player = Game.GetPlayer() 14 | local from, forward = Game.GetTargetingSystem():GetCrosshairData(player) 15 | local to = Vector4.new( 16 | from.x + forward.x * distance, 17 | from.y + forward.y * distance, 18 | from.z + forward.z * distance, 19 | from.w 20 | ) 21 | 22 | local filters = { 23 | 'Dynamic', -- Movable Objects 24 | 'Vehicle', 25 | 'Static', -- Buildings, Concrete Roads, Crates, etc. 26 | 'Water', 27 | 'Terrain', 28 | 'PlayerBlocker', -- Trees, Billboards, Barriers 29 | } 30 | 31 | local results = {} 32 | 33 | for _, filter in ipairs(filters) do 34 | local success, result = Game.GetSpatialQueriesSystem():SyncRaycastByCollisionGroup(from, to, filter, false, false) 35 | 36 | if success then 37 | table.insert(results, { 38 | distance = Vector4.Distance(from, ToVector4(result.position)), 39 | position = ToVector4(result.position), 40 | normal = result.normal, 41 | material = result.material, 42 | collision = CName.new(filter), 43 | }) 44 | end 45 | end 46 | 47 | if #results == 0 then 48 | return nil 49 | end 50 | 51 | local nearest = results[1] 52 | 53 | for i = 2, #results do 54 | if results[i].distance < nearest.distance then 55 | nearest = results[i] 56 | end 57 | end 58 | 59 | return nearest.position 60 | end 61 | 62 | ---@param searchFilter gameTargetSearchFilter|nil 63 | ---@return gameObject 64 | function TargetingHelper.GetLookAtTarget(searchFilter) 65 | local player = Game.GetPlayer() 66 | 67 | local searchQuery = TargetSearchQuery.new() 68 | searchQuery.searchFilter = searchFilter or Game['TSF_NPC;']() 69 | searchQuery.maxDistance = SNameplateRangesData.GetMaxDisplayRange() 70 | 71 | return Game.GetTargetingSystem():GetObjectClosestToCrosshair(player, searchQuery) 72 | end 73 | 74 | ---@param searchFilter gameTargetSearchFilter|nil 75 | ---@return gameObject[] 76 | function TargetingHelper.GetLookAtTargets(searchFilter) 77 | local player = Game.GetPlayer() 78 | 79 | local searchQuery = TargetSearchQuery.new() 80 | searchQuery.searchFilter = searchFilter or Game['TSF_NPC;']() 81 | searchQuery.maxDistance = SNameplateRangesData.GetMaxDisplayRange() 82 | 83 | local success, targetParts = Game.GetTargetingSystem():GetTargetParts(player, searchQuery) 84 | local targets = {} 85 | 86 | if success then 87 | for _, targetPart in ipairs(targetParts) do 88 | local component = targetPart:GetComponent() 89 | 90 | local target = component:GetEntity() 91 | local targetId = tostring(target:GetEntityID().hash) 92 | 93 | targets[targetId] = target 94 | end 95 | end 96 | 97 | return targets 98 | end 99 | 100 | ---@param target gameObject 101 | ---@return boolean 102 | function TargetingHelper.IsActive(target) 103 | return target:IsAttached() 104 | and not target:IsDeadNoStatPool() 105 | and not target:IsTurnedOffNoStatusEffect() 106 | and not ScriptedPuppet.IsDefeated(target) 107 | and not ScriptedPuppet.IsUnconscious(target) 108 | end 109 | 110 | ---@param target gameObject 111 | ---@return string 112 | function TargetingHelper.GetTargetId(target) 113 | return tostring(target:GetEntityID().hash) 114 | end 115 | 116 | ---@param target gameObject 117 | ---@return boolean 118 | function TargetingHelper.IsTargetMarked(target) 119 | local targetId = TargetingHelper.GetTargetId(target) 120 | 121 | return markers[targetId] ~= nil 122 | end 123 | 124 | ---@param target gameObject 125 | function TargetingHelper.MarkTarget(target) 126 | local targetId = TargetingHelper.GetTargetId(target) 127 | 128 | local mappinData = MappinData.new() 129 | mappinData.mappinType = 'Mappins.DefaultStaticMappin' 130 | mappinData.variant = gamedataMappinVariant.TakeControlVariant 131 | mappinData.visibleThroughWalls = true 132 | 133 | local mappinId = Game.GetMappinSystem():RegisterMappinWithObject(mappinData, target, 'poi_mappin', Vector3.new(0, 0, 2.0)) 134 | 135 | markers[targetId] = { target = target, mappinId = mappinId } 136 | end 137 | 138 | ---@param target gameObject 139 | function TargetingHelper.UnmarkTarget(target) 140 | local targetId = TargetingHelper.GetTargetId(target) 141 | 142 | if markers[targetId] then 143 | local marker = markers[targetId] 144 | 145 | Game.GetMappinSystem():UnregisterMappin(marker.mappinId) 146 | 147 | markers[targetId] = nil 148 | end 149 | end 150 | 151 | ---@param autoClear boolean 152 | ---@return gameObject[] 153 | function TargetingHelper.GetMarkedTargets(autoClear) 154 | -- Auto clear is ON by default 155 | if autoClear == nil then 156 | autoClear = true 157 | end 158 | 159 | local targets = {} 160 | 161 | for _, marker in pairs(markers) do 162 | if TargetingHelper.IsActive(marker.target) then 163 | table.insert(targets, marker.target) 164 | elseif autoClear then 165 | TargetingHelper.UnmarkTarget(marker.target) 166 | end 167 | end 168 | 169 | return targets 170 | end 171 | 172 | function TargetingHelper.UnmarkTargets() 173 | for _, marker in pairs(markers) do 174 | Game.GetMappinSystem():UnregisterMappin(marker.mappinId) 175 | end 176 | 177 | markers = {} 178 | end 179 | 180 | ---@param position Vector4 181 | ---@param variant gamedataMappinVariant 182 | function TargetingHelper.MarkPosition(position, variant) 183 | local positionId = tostring(position) 184 | 185 | local mappinData = MappinData.new() 186 | mappinData.mappinType = 'Mappins.DefaultStaticMappin' 187 | mappinData.variant = variant or gamedataMappinVariant.AimVariant 188 | mappinData.visibleThroughWalls = true 189 | 190 | local mappinId = Game.GetMappinSystem():RegisterMappin(mappinData, position) 191 | 192 | pins[positionId] = { position = position, mappinId = mappinId } 193 | end 194 | 195 | ---@param position Vector4 196 | function TargetingHelper.UnmarkPosition(position) 197 | local positionId = tostring(position) 198 | 199 | if pins[positionId] then 200 | local pin = pins[positionId] 201 | 202 | Game.GetMappinSystem():UnregisterMappin(pin.mappinId) 203 | 204 | pins[positionId] = nil 205 | end 206 | end 207 | 208 | function TargetingHelper.UnmarkPositions() 209 | for _, pin in pairs(pins) do 210 | Game.GetMappinSystem():UnregisterMappin(pin.mappinId) 211 | end 212 | 213 | pins = {} 214 | end 215 | 216 | function TargetingHelper.Dispose() 217 | TargetingHelper.UnmarkTargets() 218 | TargetingHelper.UnmarkPositions() 219 | end 220 | 221 | return TargetingHelper -------------------------------------------------------------------------------- /ai-components/init.lua: -------------------------------------------------------------------------------- 1 | local TargetingHelper = require('TargetingHelper') 2 | local AIControl = require('AIControl') 3 | 4 | registerHotkey('SelectNPC', 'Mark / Unmark NPC', function() 5 | local target = TargetingHelper.GetLookAtTarget() 6 | 7 | if target and target:IsNPC() then 8 | if TargetingHelper.IsTargetMarked(target) then 9 | TargetingHelper.UnmarkTarget(target) 10 | else 11 | TargetingHelper.MarkTarget(target) 12 | end 13 | end 14 | end) 15 | 16 | registerHotkey('MoveMarkedNPC', 'Send marked NPCs to palyer', function() 17 | local targets = TargetingHelper.GetMarkedTargets() 18 | local movePosition = TargetingHelper.GetLookAtPosition() 19 | 20 | if #targets == 0 or not movePosition then 21 | return 22 | end 23 | 24 | local player = Game.GetPlayer() 25 | local moveOffsetX, moveOffsetY = 0, 0.5 26 | 27 | for _, target in pairs(targets) do 28 | -- Make NPC react faster to the next command 29 | -- before the first command is in the chain 30 | if not AIControl.HasQueue(target) then 31 | AIControl.InterruptBehavior(target) 32 | end 33 | 34 | -- Clone position for closures 35 | local pinPosition = ToVector4(movePosition) 36 | 37 | -- Place a pin that would be removed when task is completed 38 | TargetingHelper.MarkPosition(pinPosition) 39 | 40 | -- Move to the position while looking at the player 41 | AIControl.QueueTask(target, function() 42 | AIControl.LookAt(target, player) 43 | 44 | return AIControl.MoveTo(target, movePosition) 45 | end) 46 | 47 | -- Rotate to the player on arrival 48 | AIControl.QueueTask(target, function() 49 | TargetingHelper.UnmarkPosition(pinPosition) 50 | 51 | return AIControl.RotateTo(target, player:GetWorldPosition()) 52 | end) 53 | 54 | -- Stay for a sec after reaching a position 55 | AIControl.QueueTask(target, function() 56 | return AIControl.HoldFor(target, 1.0) 57 | end) 58 | 59 | -- Stop looking at the player 60 | AIControl.QueueTask(target, function() 61 | AIControl.StopLookAt(target) 62 | end) 63 | 64 | -- Give next NPC some space 65 | movePosition.x = movePosition.x + moveOffsetX 66 | movePosition.y = movePosition.y + moveOffsetY 67 | 68 | moveOffsetX, moveOffsetY = moveOffsetY, moveOffsetX 69 | end 70 | end) 71 | 72 | registerHotkey('TeleportMarkedNPC', 'Teleport marked NPCs to palyer', function() 73 | local targets = TargetingHelper.GetMarkedTargets() 74 | local teleportPosition = TargetingHelper.GetLookAtPosition() 75 | 76 | if #targets == 0 or not teleportPosition then 77 | return 78 | end 79 | 80 | local teleportOffsetX, teleportOffsetY = 0, 0.5 81 | 82 | for _, target in pairs(targets) do 83 | AIControl.TeleportTo(target, teleportPosition) 84 | AIControl.HoldFor(target, 3.0) -- Stay for 3 secs after teleport 85 | 86 | -- Give next NPC some space 87 | teleportPosition.x = teleportPosition.x + teleportOffsetX 88 | teleportPosition.y = teleportPosition.y + teleportOffsetY 89 | 90 | teleportOffsetX, teleportOffsetY = teleportOffsetY, teleportOffsetX 91 | end 92 | end) 93 | 94 | registerHotkey('RecruitFollower', 'Recruit follower', function() 95 | local target = TargetingHelper.GetLookAtTarget() 96 | 97 | if target and target:IsNPC() then 98 | AIControl.InterruptCombat(target) 99 | AIControl.MakeFollower(target) 100 | end 101 | end) 102 | 103 | registerHotkey('RecruitEveryone', 'Recruit everyone', function() 104 | local targets = TargetingHelper.GetLookAtTargets() 105 | 106 | for _, target in pairs(targets) do 107 | AIControl.InterruptCombat(target) 108 | AIControl.MakeFollower(target) 109 | end 110 | end) 111 | 112 | registerHotkey('StartMassacre', 'Start massacre', function() 113 | local targets = TargetingHelper.GetLookAtTargets() 114 | 115 | for _, target in pairs(targets) do 116 | AIControl.MakePsycho(target) 117 | end 118 | end) 119 | 120 | registerForEvent('onInit', function() 121 | -- Free follower when NPC is detached 122 | Observe('ScriptedPuppet', 'OnDetach', function(self) 123 | if self and self:IsA('NPCPuppet') then 124 | TargetingHelper.UnmarkTarget(self) 125 | AIControl.FreeFollower(self) 126 | end 127 | end) 128 | 129 | -- Maintain the correct state on session end 130 | Observe('QuestTrackerGameController', 'OnUninitialize', function() 131 | if Game.GetPlayer() == nil then 132 | TargetingHelper.Dispose() 133 | AIControl.Dispose() 134 | end 135 | end) 136 | end) 137 | 138 | -- Maintain the correct state on "Reload All Mods" 139 | registerForEvent('onShutdown', function() 140 | TargetingHelper.Dispose() 141 | AIControl.Dispose() 142 | end) 143 | 144 | registerForEvent('onUpdate', function(delta) 145 | AIControl.UpdateTasks(delta) 146 | end) 147 | -------------------------------------------------------------------------------- /mappin-system/README.md: -------------------------------------------------------------------------------- 1 | # Mappin System Example 2 | 3 | ### Features 4 | 5 | - Place a map pin at the player's current position 6 | - Place a map pin on an object under the crosshair (NPC, Car, Terminal, etc.) 7 | 8 | ### Notes 9 | 10 | A map pin can be tracked (drawing path in the map and minimap) if the variant allowing it. 11 | A map pin placed on an object follows the object if it moves. 12 | 13 | Custom map pins remain after fast traveling. 14 | Although the "pinned" object can be disposed / teleported, 15 | in which case the pin will move to an unpredictable coordinate. 16 | 17 | ### References 18 | 19 | - [Mappin System](https://redscript.redmodding.org/#24572) 20 | - [Mappin Variants](https://github.com/WolvenKit/CyberCAT/blob/main/CyberCAT.Core/Enums/Dumped%20Enums/gamedataMappinVariant.cs) 21 | - [Targeting System](https://redscript.redmodding.org/#21605) 22 | -------------------------------------------------------------------------------- /mappin-system/init.lua: -------------------------------------------------------------------------------- 1 | registerHotkey('PlaceCustomMapPin', 'Place a map pin at player\'s position', function() 2 | local mappinData = MappinData.new() 3 | mappinData.mappinType = 'Mappins.DefaultStaticMappin' 4 | mappinData.variant = gamedataMappinVariant.FastTravelVariant 5 | mappinData.visibleThroughWalls = true 6 | 7 | local position = Game.GetPlayer():GetWorldPosition() 8 | 9 | Game.GetMappinSystem():RegisterMappin(mappinData, position) 10 | end) 11 | 12 | registerHotkey('PlaceObjectMapPin', 'Place a map pin on the target', function() 13 | local target = Game.GetTargetingSystem():GetLookAtObject(Game.GetPlayer(), false, false) 14 | 15 | if target then 16 | local mappinData = MappinData.new() 17 | mappinData.mappinType = 'Mappins.DefaultStaticMappin' 18 | mappinData.variant = gamedataMappinVariant.FastTravelVariant 19 | mappinData.visibleThroughWalls = true 20 | 21 | local slot = CName.new('poi_mappin') 22 | 23 | -- Move the pin a bit up relative to the target 24 | -- Approx. position over the NPC head 25 | local offset = Vector3.new(0, 0, 2) 26 | 27 | Game.GetMappinSystem():RegisterMappinWithObject(mappinData, target, slot, offset) 28 | end 29 | end) 30 | -------------------------------------------------------------------------------- /mod-override/ModOverride.lua: -------------------------------------------------------------------------------- 1 | local ModOverride = {} 2 | 3 | local ready = false 4 | local items = {} 5 | 6 | -- Find all iconic pairs of weapon + mod 7 | local function discoverIconicMods() 8 | local iconicMods = {} 9 | local iconicModifer = TweakDB:GetRecord('Quality.IconicItem') 10 | local weaponRecords = TweakDB:GetRecords('gamedataWeaponItem_Record') 11 | 12 | for _, weaponRecord in pairs(weaponRecords) do 13 | local weaponTag = weaponRecord:GetVisualTagsItem(0).value 14 | 15 | if not iconicMods[weaponTag] and weaponRecord:StatModifiersContains(iconicModifer) then 16 | for index = 0, weaponRecord:GetSlotPartListPresetCount() - 1 do 17 | local slotItemPartPreset = weaponRecord:GetSlotPartListPresetItem(index) 18 | 19 | if slotItemPartPreset:Slot():EntitySlotName() == 'IconicWeaponModLegendary' then 20 | local modRecord = slotItemPartPreset:ItemPartPreset() 21 | 22 | iconicMods[weaponTag] = { 23 | modRecord = modRecord, 24 | weaponTag = weaponTag, 25 | weaponRecord = weaponRecord 26 | } 27 | 28 | break 29 | end 30 | end 31 | end 32 | end 33 | 34 | return iconicMods 35 | end 36 | 37 | -- Make given mods available for player 38 | local function unlockIconicMods(iconicMods) 39 | local slots = { 40 | ['AttachmentSlots.GenericWeaponMod'] = 4, 41 | ['AttachmentSlots.MeleeWeaponMod'] = 3, 42 | } 43 | 44 | for _, iconic in pairs(iconicMods) do 45 | local modId = iconic.modRecord:GetID() 46 | local weaponId = iconic.weaponRecord:GetID() 47 | local weaponName = TweakDB:GetFlat(weaponId .. '.displayName') 48 | local slotList = TweakDB:GetFlat(modId .. '.placementSlots') 49 | 50 | -- Iconic mods are allowed to be installed in only one slot 51 | -- If there is more than one slot then TweakDB is modified 52 | if #slotList == 1 then 53 | for slotGroup, slotCount in pairs(slots) do 54 | for index = 1, slotCount do 55 | local slotId = TweakDBID.new(slotGroup .. index) 56 | local slotUnlocked = false 57 | 58 | for _, unlockedId in ipairs(slotList) do 59 | if slotId == unlockedId then 60 | slotUnlocked = true 61 | break 62 | end 63 | end 64 | 65 | if not slotUnlocked then 66 | table.insert(slotList, slotId) 67 | end 68 | end 69 | end 70 | 71 | TweakDB:SetFlatNoUpdate(modId .. '.displayName', weaponName) 72 | TweakDB:SetFlatNoUpdate(modId .. '.placementSlots', slotList) 73 | TweakDB:Update(modId) 74 | end 75 | end 76 | end 77 | 78 | local function prepareItemList(iconicMods) 79 | items = {} 80 | 81 | for _, iconic in pairs(iconicMods) do 82 | local weaponName = Game.GetLocalizedTextByKey(iconic.weaponRecord:DisplayName()) 83 | local weaponDesc = Game.GetLocalizedTextByKey(iconic.weaponRecord:LocalizedDescription()) 84 | local abilityDesc = Game.GetLocalizedText(iconic.modRecord:GetOnAttachItem(0):UIData():LocalizedDescription()) 85 | local weaponTag = iconic.weaponTag:gsub('_', ' '):gsub('^' .. weaponName .. ' ', '') 86 | 87 | local item = {} 88 | 89 | item.recorId = iconic.modRecord:GetID() 90 | item.label = ('%s %q'):format(weaponTag, weaponName:gsub('%%', '%%%%')) 91 | 92 | item.abilityDesc = abilityDesc:gsub('%%', '%%%%') 93 | item.weaponDesc = weaponDesc:gsub('%%', '%%%%') 94 | 95 | item.filter = item.label:upper() 96 | 97 | table.insert(items, item) 98 | end 99 | 100 | table.sort(items, function(a, b) 101 | return a.label < b.label 102 | end) 103 | end 104 | 105 | local function unlockScopes(targetList, sourceList) 106 | local targetParts = TweakDB:GetFlat(targetList .. '.itemPartList') 107 | local sourceParts = TweakDB:GetFlat(sourceList .. '.itemPartList') 108 | 109 | for _, sourcePartId in ipairs(sourceParts) do 110 | local isPartUnlocked = false 111 | 112 | for _, targetPartId in ipairs(targetParts) do 113 | if sourcePartId == targetPartId then 114 | isPartUnlocked = true 115 | break 116 | end 117 | end 118 | 119 | if not isPartUnlocked then 120 | table.insert(targetParts, sourcePartId) 121 | end 122 | end 123 | 124 | TweakDB:SetFlatNoUpdate(targetList .. '.itemPartList', targetParts) 125 | TweakDB:Update(targetList) 126 | end 127 | 128 | local function unlockAllScopes() 129 | unlockScopes('Items.HandgunPossibleScopesList', 'Items.RiflePossibleScopesList') 130 | unlockScopes('Items.HandgunPossibleScopesList', 'Items.SniperPossibleScopesList') 131 | unlockScopes('Items.RiflePossibleScopesList', 'Items.SniperPossibleScopesList') 132 | end 133 | 134 | function ModOverride.Init() 135 | local iconicMods = discoverIconicMods() 136 | unlockIconicMods(iconicMods) 137 | prepareItemList(iconicMods) 138 | 139 | unlockAllScopes() 140 | 141 | local player = Game.GetPlayer() 142 | local isPreGame = Game.GetSystemRequestsHandler():IsPreGame() 143 | ready = player and player:IsAttached() and not isPreGame 144 | 145 | Observe('QuestTrackerGameController', 'OnInitialize', function() 146 | ready = true 147 | end) 148 | 149 | Observe('QuestTrackerGameController', 'OnUninitialize', function() 150 | ready = Game.GetPlayer() ~= nil 151 | end) 152 | end 153 | 154 | function ModOverride.IsReady() 155 | return ready 156 | end 157 | 158 | function ModOverride.GetItems(filter) 159 | if not filter or filter == '' then 160 | return items 161 | end 162 | 163 | local filterEsc = filter:gsub('([^%w])', '%%%1'):upper() 164 | local filterRe = filterEsc:gsub('%s+', '.* ') .. '.*' 165 | 166 | local filtered = {} 167 | 168 | for _, item in ipairs(items) do 169 | if item.filter:find(filterRe) then 170 | table.insert(filtered, item) 171 | end 172 | end 173 | 174 | return filtered 175 | end 176 | 177 | return ModOverride -------------------------------------------------------------------------------- /mod-override/README.md: -------------------------------------------------------------------------------- 1 | # Weapon Mod Override Example 2 | 3 | ### Features 4 | 5 | - Install Iconic weapons effects in the regular mod slots 6 | - Install sniper scopes on handguns and rifles 7 | - Install rifle scopes on handguns 8 | - Traverse TweakDB records to find all Iconic mods 9 | - Update TweakDB records to make Iconic mods installable into weapon slots 10 | - Decoupling of UI and logic 11 | - UI scaling based on user font size setting 12 | - Filterable list for adding mods to the inventory 13 | 14 | ### References 15 | 16 | - TweakDB [PR 1](https://github.com/yamashi/CyberEngineTweaks/pull/461) + [PR 2](https://github.com/yamashi/CyberEngineTweaks/pull/524) 17 | - Localized Text 18 | - Override / Observe [Patch Notes](https://wiki.cybermods.net/cyber-engine-tweaks/patch-notes#1-11-1-15-02-2021) 19 | - [ImGui](https://github.com/yamashi/CyberEngineTweaks/tree/master/src/sol_imgui) 20 | -------------------------------------------------------------------------------- /mod-override/UI.lua: -------------------------------------------------------------------------------- 1 | local UI = {} 2 | 3 | local style = {} 4 | 5 | local state = { 6 | open = false, 7 | filter = '', 8 | selected = nil, 9 | } 10 | 11 | local logic = { 12 | isReady = function() return false end, 13 | getItemList = function() return {} end, 14 | addToInventory = function() end, 15 | } 16 | 17 | function UI.Init() 18 | style.scale = ImGui.GetFontSize() / 13 19 | 20 | style.windowWidth = 340 * style.scale 21 | style.windowHeight = 0 -- Auto height 22 | style.windowPaddingX = 8 * style.scale 23 | style.windowPaddingY = 8 * style.scale 24 | 25 | style.framePaddingX = 3 * style.scale 26 | style.framePaddingY = 3 * style.scale 27 | style.innerSpacingX = 4 * style.scale 28 | style.innerSpacingY = 4 * style.scale 29 | style.itemSpacingX = 8 * style.scale 30 | style.itemSpacingY = 4 * style.scale 31 | 32 | style.listBoxHeight = (7 * 17 - 2) * style.scale 33 | style.buttonHeight = 20 * style.scale 34 | 35 | local screenWidth, screenHeight = GetDisplayResolution() 36 | 37 | style.windowX = (screenWidth - style.windowWidth) / 2 38 | style.windowY = (screenHeight - style.windowHeight) / 2 39 | end 40 | 41 | function UI.OnReadyCheck(callback) 42 | if type(callback) == 'function' then 43 | logic.isReady = callback 44 | end 45 | end 46 | 47 | function UI.OnListItems(callback) 48 | if type(callback) == 'function' then 49 | logic.getItemList = callback 50 | end 51 | end 52 | 53 | function UI.OnAddToInventory(callback) 54 | if type(callback) == 'function' then 55 | logic.addToInventory = callback 56 | end 57 | end 58 | 59 | function UI.Show() 60 | state.open = true 61 | end 62 | 63 | function UI.Hide() 64 | state.open = false 65 | end 66 | 67 | function UI.Draw() 68 | if not state.open then 69 | return 70 | end 71 | 72 | ImGui.SetNextWindowPos(style.windowX, style.windowY, ImGuiCond.FirstUseEver) 73 | ImGui.SetNextWindowSize(style.windowWidth + style.windowPaddingX * 2 - 1, style.windowHeight) 74 | 75 | ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, style.windowPaddingX, style.windowPaddingY) 76 | ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, style.framePaddingX, style.framePaddingY) 77 | ImGui.PushStyleVar(ImGuiStyleVar.ItemInnerSpacing, style.innerSpacingX, style.innerSpacingY) 78 | ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, style.itemSpacingX, style.itemSpacingY) 79 | 80 | if ImGui.Begin('Iconic Mods', ImGuiWindowFlags.NoResize + ImGuiWindowFlags.NoScrollbar + ImGuiWindowFlags.NoScrollWithMouse) then 81 | if logic.isReady() then 82 | ImGui.SetNextItemWidth(style.windowWidth) 83 | ImGui.PushStyleColor(ImGuiCol.TextDisabled, 0xffaaaaaa) 84 | state.filter = ImGui.InputTextWithHint('##ItemFilter', 'Filter mods...', state.filter, 100) 85 | ImGui.PopStyleColor() 86 | 87 | ImGui.Spacing() 88 | 89 | ImGui.PushStyleColor(ImGuiCol.FrameBg, 0) 90 | ImGui.PushStyleVar(ImGuiStyleVar.FrameBorderSize, 0) 91 | ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, 0, 0) 92 | ImGui.BeginListBox('##ItemList', style.windowWidth, style.listBoxHeight) 93 | 94 | for _, item in ipairs(logic.getItemList(state.filter)) do 95 | if ImGui.Selectable(item.label, (state.selected == item)) then 96 | state.selected = item 97 | end 98 | end 99 | 100 | ImGui.EndListBox() 101 | ImGui.PopStyleVar(2) 102 | ImGui.PopStyleColor() 103 | 104 | if state.selected then 105 | ImGui.Spacing() 106 | ImGui.Separator() 107 | ImGui.Spacing() 108 | 109 | ImGui.PushStyleColor(ImGuiCol.Text, 0xfffefd01) 110 | ImGui.Text(state.selected.label) 111 | ImGui.PopStyleColor() 112 | 113 | ImGui.PushStyleColor(ImGuiCol.Text, 0xff484ad5) 114 | ImGui.TextWrapped(state.selected.weaponDesc) 115 | ImGui.PopStyleColor() 116 | 117 | ImGui.PushStyleColor(ImGuiCol.Text, 0xff9f9f9f) 118 | ImGui.TextWrapped(state.selected.abilityDesc) 119 | ImGui.PopStyleColor() 120 | 121 | ImGui.Spacing() 122 | 123 | if ImGui.Button('Add to inventory', style.windowWidth, style.buttonHeight) then 124 | logic.addToInventory(state.selected) 125 | end 126 | end 127 | else 128 | ImGui.Spacing() 129 | ImGui.PushStyleColor(ImGuiCol.Text, 0xff9f9f9f) 130 | ImGui.TextWrapped('Load the game to access the iconic mods') 131 | ImGui.PopStyleColor() 132 | ImGui.Spacing() 133 | end 134 | else 135 | state.selected = nil 136 | end 137 | 138 | ImGui.End() 139 | 140 | ImGui.PopStyleVar(4) 141 | end 142 | 143 | return UI -------------------------------------------------------------------------------- /mod-override/init.lua: -------------------------------------------------------------------------------- 1 | local ModOverride = require('ModOverride') 2 | local UI = require('UI') 3 | 4 | registerForEvent('onInit', function() 5 | ModOverride.Init() 6 | 7 | UI.Init() 8 | 9 | UI.OnReadyCheck(ModOverride.IsReady) 10 | 11 | UI.OnListItems(function(filter) 12 | return ModOverride.GetItems(filter) 13 | end) 14 | 15 | UI.OnAddToInventory(function(iconicMod) 16 | local player = Game.GetPlayer() 17 | local itemId = ItemID.FromTDBID(iconicMod.recorId) 18 | 19 | Game.GetTransactionSystem():GiveItem(player, itemId, 1) 20 | end) 21 | end) 22 | 23 | registerForEvent('onOverlayOpen', function() 24 | UI.Show() 25 | end) 26 | 27 | registerForEvent('onOverlayClose', function() 28 | UI.Hide() 29 | end) 30 | 31 | registerForEvent('onDraw', function() 32 | UI.Draw() 33 | end) 34 | -------------------------------------------------------------------------------- /player-actions/README.md: -------------------------------------------------------------------------------- 1 | # Player Actions Example 2 | 3 | ### Features 4 | 5 | - Read and log all player inputs 6 | - Toggle logging 7 | 8 | ### References 9 | 10 | - [ListenerAction](https://redscript.redmodding.org/#31603) 11 | - [ActionType](https://redscript.redmodding.org/#5923) 12 | - Observe / Override 13 | -------------------------------------------------------------------------------- /player-actions/init.lua: -------------------------------------------------------------------------------- 1 | -- General logging switch 2 | local enableLogging = require('state') 3 | 4 | if enableLogging ~= false then 5 | enableLogging = true 6 | end 7 | 8 | -- Custom action filter 9 | local ignoreActions = { 10 | ['BUTTON_RELEASED'] = { 11 | ['UI_FakeMovement'] = true, 12 | }, 13 | ['RELATIVE_CHANGE'] = { 14 | ['UI_FakeCamera'] = true, 15 | ['CameraMouseX'] = true, 16 | ['CameraMouseY'] = true, 17 | ['mouse_x'] = true, 18 | ['mouse_y'] = true, 19 | }, 20 | } 21 | 22 | local function printLoggingState() 23 | print('Player action logging: ' .. (enableLogging and 'ON' or 'OFF')) 24 | end 25 | 26 | registerForEvent('onInit', function() 27 | printLoggingState() 28 | 29 | local player = Game.GetPlayer() 30 | player:RegisterInputListener(player) 31 | 32 | Observe('PlayerPuppet', 'OnGameAttached', function(self) 33 | self:RegisterInputListener(self) 34 | end) 35 | 36 | Observe('PlayerPuppet', 'OnAction', function(_, action) 37 | if enableLogging then 38 | local actionName = Game.NameToString(action:GetName()) 39 | local actionType = action:GetType().value -- gameinputActionType 40 | local actionValue = action:GetValue() 41 | 42 | if not ignoreActions[actionType] or not ignoreActions[actionType][actionName] then 43 | spdlog.info(('[%s] %s = %.3f'):format(actionType, actionName, actionValue)) 44 | end 45 | end 46 | end) 47 | end) 48 | 49 | registerHotkey('ToggleLog', 'Toggle logging', function() 50 | enableLogging = not enableLogging 51 | printLoggingState() 52 | 53 | local stateFile = io.open('state.lua', 'w') 54 | 55 | if stateFile then 56 | stateFile:write('return ') 57 | stateFile:write(tostring(enableLogging)) 58 | stateFile:close() 59 | end 60 | end) 61 | 62 | registerHotkey('FlushLog', 'Flush input log', function() 63 | spdlog.error('---') 64 | end) 65 | 66 | --[[ 67 | An example of reading specific player actions 68 | 69 | registerForEvent('onInit', function() 70 | Observe('PlayerPuppet', 'OnAction', function(_, action) 71 | local actionName = Game.NameToString(ListenerAction.GetName(action)) 72 | local actionType = ListenerAction.GetType(action).value -- gameinputActionType 73 | local actionValue = ListenerAction.GetValue(action) 74 | 75 | if actionName == 'Forward' or actionName == 'Back' then 76 | if actionType == 'BUTTON_PRESSED' then 77 | print('[Action]', actionName, 'Pressed') 78 | elseif actionType == 'BUTTON_RELEASED' then 79 | print('[Action]', actionName, 'Released') 80 | end 81 | elseif actionName == 'MoveY' then 82 | if actionValue ~= 0 then 83 | print('[Action]', (actionValue > 0 and 'Forward' or 'Back'), Game.GetPlayer():GetWorldForward()) 84 | end 85 | elseif actionName == 'Jump' then 86 | if actionType == 'BUTTON_PRESSED' then 87 | print('[Action] Jump Pressed') 88 | elseif actionType == 'BUTTON_HOLD_COMPLETE' then 89 | print('[Action] Jump Charged') 90 | elseif actionType == 'BUTTON_RELEASED' then 91 | print('[Action] Jump Released') 92 | end 93 | elseif actionName == 'WeaponSlot1' then 94 | if actionType == 'BUTTON_PRESSED' then 95 | print('[Action] Select Weapon 1') 96 | end 97 | end 98 | end) 99 | end) 100 | ]]-- 101 | -------------------------------------------------------------------------------- /player-actions/state.lua: -------------------------------------------------------------------------------- 1 | return true -------------------------------------------------------------------------------- /settings-system/README.md: -------------------------------------------------------------------------------- 1 | # Settings System Example 2 | 3 | ### Features 4 | 5 | - Cycle through all FOV options 6 | - Cycle through all screen resolutions 7 | - Toggle all HUD elements 8 | - Export all user settings as lua table 9 | 10 | ### References 11 | 12 | - [User Settings](https://redscript.redmodding.org/#54251) 13 | - [Config Group](https://redscript.redmodding.org/#54330) 14 | - [Config Var](https://redscript.redmodding.org/#54291) 15 | - [Var Types](https://redscript.redmodding.org/#6847) 16 | -------------------------------------------------------------------------------- /settings-system/dump.lua: -------------------------------------------------------------------------------- 1 | -- Recursive settings exporter 2 | 3 | local function makePath(groupPath, varName) 4 | return groupPath .. '/' .. varName 5 | end 6 | 7 | local function isNameType(type) 8 | return type == 'Name' or type == 'NameList' 9 | end 10 | 11 | local function isNumberType(type) 12 | return type == 'Int' or type == 'Float' 13 | end 14 | 15 | local function isListType(type) 16 | return type == 'IntList' or type == 'FloatList' or type == 'StringList' or type == 'NameList' 17 | end 18 | 19 | local function exportVar(var) 20 | local output = {} 21 | 22 | output.path = makePath(Game.NameToString(var:GetGroupPath()), Game.NameToString(var:GetName())) 23 | output.value = var:GetValue() 24 | output.type = var:GetType().value 25 | 26 | if isNameType(output.type) then 27 | output.value = Game.NameToString(output.value) 28 | end 29 | 30 | if isNumberType(output.type) then 31 | output.min = var:GetMinValue() 32 | output.max = var:GetMaxValue() 33 | output.step = var:GetStepValue() 34 | end 35 | 36 | if isListType(output.type) then 37 | output.index = var:GetIndex() + 1 38 | output.options = var:GetValues() 39 | 40 | if isNameType(output.type) then 41 | for i, option in ipairs(output.options) do 42 | output.options[i] = Game.NameToString(option) 43 | end 44 | end 45 | end 46 | 47 | return output 48 | end 49 | 50 | local function exportVars(isPreGame, group, output) 51 | if type(group) ~= 'userdata' then 52 | group = Game.GetSettingsSystem():GetRootGroup() 53 | end 54 | 55 | if type(isPreGame) ~= 'bool' then 56 | isPreGame = Game.GetSystemRequestsHandler():IsPreGame() 57 | end 58 | 59 | if not output then 60 | output = {} 61 | end 62 | 63 | for _, var in ipairs(group:GetVars(isPreGame)) do 64 | table.insert(output, exportVar(var)) 65 | end 66 | 67 | for _, child in ipairs(group:GetGroups(isPreGame)) do 68 | exportVars(isPreGame, child, output) 69 | end 70 | 71 | table.sort(output, function(a, b) 72 | return a.path < b.path 73 | end) 74 | 75 | return output 76 | end 77 | 78 | local function exportTo(exportPath, isPreGame) 79 | local output = {} 80 | 81 | local vars = exportVars(isPreGame) 82 | 83 | for _, var in ipairs(vars) do 84 | local value = var.value 85 | local options 86 | 87 | if type(value) == 'string' then 88 | value = string.format('%q', value) 89 | end 90 | 91 | if var.options and #var.options > 1 then 92 | options = {} 93 | 94 | for i, option in ipairs(var.options) do 95 | options[i] = option 96 | end 97 | 98 | options = ' -- ' .. table.concat(options, ' | ') 99 | elseif var.step then 100 | options = (' -- %.2f to %.2f / %.2f'):format(var.min, var.max, var.step) 101 | end 102 | 103 | table.insert(output, (' ["%s"] = %s,%s'):format(var.path, value, options or '')) 104 | end 105 | 106 | table.insert(output, 1, '{') 107 | table.insert(output, '}') 108 | 109 | output = table.concat(output, '\n') 110 | 111 | if exportPath then 112 | if not exportPath:find('%.lua$') then 113 | exportPath = exportPath .. '.lua' 114 | end 115 | 116 | local exportFile = io.open(exportPath, 'w') 117 | 118 | if exportFile then 119 | exportFile:write('return ') 120 | exportFile:write(output) 121 | exportFile:close() 122 | end 123 | else 124 | return output 125 | end 126 | end 127 | 128 | return exportTo -------------------------------------------------------------------------------- /settings-system/init.lua: -------------------------------------------------------------------------------- 1 | registerHotkey('CycleFOV', 'Cycle FOV', function() 2 | local fov = Game.GetSettingsSystem():GetVar('/graphics/basic', 'FieldOfView') 3 | 4 | local value = fov:GetValue() + fov:GetStepValue() 5 | 6 | if value > fov:GetMaxValue() then 7 | value = fov:GetMinValue() 8 | end 9 | 10 | fov:SetValue(value) 11 | 12 | print(('Current FOV: %.1f'):format(fov:GetValue())) 13 | end) 14 | 15 | registerHotkey('CycleResolution', 'Cycle resolution', function() 16 | local resolution = Game.GetSettingsSystem():GetVar('/video/display', 'Resolution') 17 | 18 | local options = resolution:GetValues() 19 | local current = resolution:GetIndex() + 1 -- lua tables start at 1 20 | local next = current + 1 21 | 22 | if next > #options then 23 | next = 1 24 | end 25 | 26 | resolution:SetIndex(next - 1) 27 | 28 | Game.GetSettingsSystem():ConfirmChanges() 29 | 30 | print(('Switched resolution from %s to %s'):format(options[current], options[next])) 31 | end) 32 | 33 | registerHotkey('ToggleHUD', 'Toggle HUD', function() 34 | local settingsSystem = Game.GetSettingsSystem() 35 | 36 | local hudGroup = settingsSystem:GetGroup('/interface/hud') 37 | local newState = not hudGroup:GetVar('healthbar'):GetValue() 38 | 39 | for _, var in ipairs(hudGroup:GetVars(false)) do 40 | var:SetValue(newState) 41 | end 42 | end) 43 | 44 | registerHotkey('ExportSettings', 'Export all settings', function() 45 | require('dump')('settings.lua') 46 | end) 47 | -------------------------------------------------------------------------------- /stash-anywhere/README.md: -------------------------------------------------------------------------------- 1 | # Stash Anywhere Example 2 | 3 | ### Features 4 | 5 | - Open the main stash at any moment in the game 6 | - Iterate and print all items in the main stash 7 | -------------------------------------------------------------------------------- /stash-anywhere/init.lua: -------------------------------------------------------------------------------- 1 | local StashAnywhere = {} 2 | 3 | ---@return Stash 4 | function StashAnywhere.GetStashEntity() 5 | return Game.FindEntityByID(EntityID.new({ hash = 16570246047455160070ULL })) 6 | end 7 | 8 | ---@return gameItemData[] 9 | function StashAnywhere.GetStashItems() 10 | local stash = StashAnywhere.GetStashEntity() 11 | 12 | if stash then 13 | local success, items = Game.GetTransactionSystem():GetItemList(stash) 14 | 15 | if success then 16 | return items 17 | end 18 | end 19 | 20 | return {} 21 | end 22 | 23 | function StashAnywhere.OpenStashMenu() 24 | local stash = StashAnywhere.GetStashEntity() 25 | 26 | if stash then 27 | local openStashEvent = OpenStash.new() 28 | openStashEvent:SetProperties() 29 | 30 | stash:OnOpenStash(openStashEvent) 31 | end 32 | end 33 | 34 | registerHotkey('OpenStash', 'Open stash', function() 35 | StashAnywhere.OpenStashMenu() 36 | end) 37 | 38 | registerHotkey('PrintStash', 'Print stash items', function() 39 | local items = StashAnywhere.GetStashItems() 40 | 41 | if #items > 0 then 42 | print('[Stash] Total Items:', #items) 43 | 44 | for _, itemData in pairs(items) do 45 | local itemID = itemData:GetID() 46 | local recordID = itemID.id 47 | 48 | print('[Stash]', Game.GetLocalizedTextByKey(TDB.GetLocKey(recordID .. '.displayName'))) 49 | end 50 | end 51 | end) 52 | 53 | return StashAnywhere -------------------------------------------------------------------------------- /vehicle-system/README.md: -------------------------------------------------------------------------------- 1 | # Vehicle System Example 2 | 3 | ### Features 4 | 5 | - Unlock vehicles to add to the Call List (aka garage) 6 | - Spawn a vehicle from the Call List 7 | 8 | ### References 9 | 10 | - [Vehicle System](https://redscript.redmodding.org/#61532) 11 | - TweakDB [Link 1](https://github.com/yamashi/CyberEngineTweaks/pull/461) + [Link 2](https://github.com/yamashi/CyberEngineTweaks/pull/524) 12 | -------------------------------------------------------------------------------- /vehicle-system/init.lua: -------------------------------------------------------------------------------- 1 | -- The list of the vehicles to add to the call list 2 | local targetVehicles = { 3 | 'Vehicle.v_standard2_archer_hella_police', 4 | 'Vehicle.v_standard2_villefort_cortes_police', 5 | 'Vehicle.v_standard3_chevalier_emperor_police', 6 | 'Vehicle.v_standard2_archer_hella_player', 7 | 'Vehicle.v_sport2_quadra_type66_nomad', 8 | } 9 | 10 | local function unlockVehicles(vehicles) 11 | local unlockableVehicles = TweakDB:GetFlat('Vehicle.vehicle_list.list') 12 | 13 | for _, vehiclePath in ipairs(vehicles) do 14 | local targetVehicleTweakDbId = TweakDBID.new(vehiclePath) 15 | local isVehicleUnlockable = false 16 | 17 | for _, unlockableVehicleTweakDbId in ipairs(unlockableVehicles) do 18 | if unlockableVehicleTweakDbId == targetVehicleTweakDbId then 19 | isVehicleUnlockable = true 20 | break 21 | end 22 | end 23 | 24 | if not isVehicleUnlockable then 25 | table.insert(unlockableVehicles, targetVehicleTweakDbId) 26 | end 27 | end 28 | 29 | TweakDB:SetFlat('Vehicle.vehicle_list.list', unlockableVehicles) 30 | end 31 | 32 | local function summonVehicle(vehiclePath) 33 | local vehicleSystem = Game.GetVehicleSystem() 34 | 35 | local garageVehicleId = GarageVehicleID.Resolve(vehiclePath) 36 | vehicleSystem:TogglePlayerActiveVehicle(garageVehicleId, gamedataVehicleType.Car, true) 37 | vehicleSystem:SpawnPlayerVehicle(gamedataVehicleType.Car) 38 | end 39 | 40 | -- If you change the vehicle list and reload the mod, 41 | -- you will also have to reload the save for the changes 42 | -- to take effect 43 | registerForEvent('onInit', function() 44 | unlockVehicles(targetVehicles) 45 | end) 46 | 47 | -- You cannot spawn the same vehicle twice with Vehicle System 48 | registerHotkey('SpawnRandomVehicle', 'Spawn a random vehicle', function() 49 | summonVehicle(targetVehicles[math.random(#targetVehicles)]) 50 | end) 51 | 52 | -- With instant summon you can control the position (in front of the player) 53 | -- Otherwise the game can spawn a vehicle right in the spot of another one 54 | registerHotkey('ToggleSpawnMode', 'Toggle instant spawn mode', function() 55 | Game.GetVehicleSystem():ToggleSummonMode() 56 | end) 57 | 58 | -- The results of this action are permanent as long 59 | -- as unlocking is done before loading into the game 60 | registerHotkey('EnableAllVehicles', 'Add vehicles to the call list', function() 61 | for _, targetVehicle in ipairs(targetVehicles) do 62 | Game.GetVehicleSystem():EnablePlayerVehicle(targetVehicle, true, false) 63 | end 64 | end) 65 | --------------------------------------------------------------------------------