├── dependencies └── client │ ├── MenuAPI.dll │ └── Newtonsoft.Json.dll ├── EnhancedCamera ├── dist │ ├── __resource.lua │ └── config.ini ├── Language.cs ├── Properties │ └── AssemblyInfo.cs ├── EnhancedCamera.csproj ├── langs │ ├── zh_CN.cs │ ├── zh_TW.cs │ └── en_US.cs ├── MainMenu.cs └── menus │ ├── DroneCam.cs │ └── CustomCam.cs ├── LICENSE ├── EnhancedCamera.sln ├── README.md └── .gitignore /dependencies/client/MenuAPI.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shrimpey/FiveM-Enhanced-Camera/HEAD/dependencies/client/MenuAPI.dll -------------------------------------------------------------------------------- /dependencies/client/Newtonsoft.Json.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shrimpey/FiveM-Enhanced-Camera/HEAD/dependencies/client/Newtonsoft.Json.dll -------------------------------------------------------------------------------- /EnhancedCamera/dist/__resource.lua: -------------------------------------------------------------------------------- 1 | resource_manifest_version '77731fab-63ca-442c-a67b-abc70f28dfa5' 2 | 3 | files { 4 | 'MenuAPI.dll', 5 | 'config.ini' 6 | } 7 | 8 | client_script { 9 | 'enhancedcamera.net.dll', 10 | 'Newtonsoft.Json.dll' 11 | } -------------------------------------------------------------------------------- /EnhancedCamera/dist/config.ini: -------------------------------------------------------------------------------- 1 | #The Control to toggle the Menu (default is 344=F11) list at https://wiki.fivem.net/wiki/Controls 2 | toggleMenu=344 3 | #Should chase camera be enabled for users to select? (1 - yes, 0 - no) 4 | chaseCameraEnabled=1 5 | #Should drone camera be enabled for users to select? (1 - yes, 0 - no) 6 | droneCameraEnabled=1 -------------------------------------------------------------------------------- /EnhancedCamera/Language.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using CitizenFX.Core; 8 | using static CitizenFX.Core.Native.API; 9 | using CustomCamera.langs; 10 | 11 | namespace CustomCamera { 12 | class Language { 13 | public static string get(string key) { 14 | if (MainMenu.langData != null) { 15 | if (MainMenu.langData.ContainsKey(key)) { 16 | return (string)MainMenu.langData[key]; 17 | } 18 | } 19 | return key; 20 | } 21 | 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /EnhancedCamera/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | 4 | [assembly: AssemblyTitle("Enhanced Camera")] 5 | [assembly: AssemblyDescription("Custom camera options")] 6 | [assembly: AssemblyConfiguration("")] 7 | [assembly: AssemblyCompany("")] 8 | [assembly: AssemblyProduct("Enhanced Camera")] 9 | [assembly: AssemblyCopyright("Copyright © 2019")] 10 | [assembly: AssemblyTrademark("")] 11 | [assembly: AssemblyCulture("")] 12 | 13 | [assembly: ComVisible(false)] 14 | 15 | [assembly: Guid("09ebf4dd-b196-4eb4-a1fe-b4aaad0b4023")] 16 | 17 | [assembly: AssemblyVersion("1.0.0.0")] 18 | [assembly: AssemblyFileVersion("1.0.0.0")] 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Luke Kabat 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 | -------------------------------------------------------------------------------- /EnhancedCamera.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26430.16 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnhancedCamera", "EnhancedCamera\EnhancedCamera.csproj", "{09EBF4DD-B196-4EB4-A1FE-B4AAAD0B4023}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {09EBF4DD-B196-4EB4-A1FE-B4AAAD0B4023}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {09EBF4DD-B196-4EB4-A1FE-B4AAAD0B4023}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {09EBF4DD-B196-4EB4-A1FE-B4AAAD0B4023}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {09EBF4DD-B196-4EB4-A1FE-B4AAAD0B4023}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {63101A63-FCD6-4BAF-A917-6641850D413D} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Enhanced Camera 2 | 3 | ## Overview of the menu 4 | This menu recreates vehicle camera from scratch and introduces a number of customizable parameters for user to tune. On top of that there is a drone camera with a few modes to simulate drone flight. 5 | 6 |

7 | 8 |

9 | 10 | Main features of this menu: 11 | - **Lead camera** - main camera, rotates around car dependant on angular velocity of the vehicle 12 | - **Chase camera** - focuses on closest vehicle and points towards it 13 | - **Drone camera** - simulates drone physics, allows choice between different modes (race, zero-G, spectator, homing) 14 | - **Customizable parameters** - FOV, XYZ position offsets, interpolation values and more 15 | - **Saving/loading** - save and load camera parameters or choose from default presets 16 | 17 | ## Installation 18 | 19 | Go to [releases](https://github.com/Shrimpey/EnhancedCamera/releases) and download latest zipped release. Unzip and place ``enhancedcamera`` folder inside your server's ``resources`` folder, then edit your server.cfg to include line ``start enhancedcamera``. 20 | 21 | ## Parameters overview 22 | 23 | It's best to just experiment with parameters to see how they affect camera handling (for lead/chase camera you can start by spawning a preset from submenu). 24 | #### Lead and chase camera parameters: 25 | - **Lock position offset** - determines whether camera changes position compared to vehicle (or just rotates) 26 | - **Linear position offset** - experimental feature, camera changes position along the line drawn behind vehicle instead of doing circular motion around the car 27 | - **Lock rotation to camera plane** - changes the way that camera rotates around car (mostly visible on uneven ground) 28 | - **Modifier** - this modifier * angular velocity = target rotation. Higher values make camera move further from lock. (-1,1) 29 | - **Yaw interpolation** - lower values - smoother movement along yaw axis (0,1) 30 | - **Roll interpolation** - lower values - smoother movement along roll axis (0,1) 31 | - **Pitch interpolation** - lower values - smoother movement along pitch axis (0,1) 32 | - **Camera offset** - offsets chase camera target towards its velocity vector. (0,5) 33 | - **Position interpolation** - lower values - smoother movement, higher delay. (0,1) 34 | - **FOV** - changes field of view, may affect performance with higher values (20,120) 35 | - **X/Y/Z Offset** - static position offset in XYZ direction 36 | - **Max angle to lock** - for chase camera only, max angle from velocity vector to keep the lock on, if angle exceeds this limit, camera switches back to normal. (25,360) 37 | 38 | ## Examples/showcase 39 | 40 | [![Showcase](https://img.youtube.com/vi/JL7sxyNec_g/0.jpg)](https://youtu.be/JL7sxyNec_g) 41 | 42 | ## Credits 43 | - [Tom Grobbe](https://github.com/TomGrobbe) - [MenuAPI](https://github.com/TomGrobbe/MenuAPI) used for GUI, code snippets from [vMenu](https://github.com/TomGrobbe/vMenu) used for saving/loading camera parameters 44 | - [QuadrupleTurbo](https://github.com/QuadrupleTurbo) - providing new ideas for features, massive help with playtesting 45 | - [No Name Drift](https://nonamedrift.com/) and [Velocity](http://www.velocitydrift.com/) servers - playtesting and feedback 46 | -------------------------------------------------------------------------------- /EnhancedCamera/EnhancedCamera.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {09EBF4DD-B196-4EB4-A1FE-B4AAAD0B4023} 8 | Library 9 | Properties 10 | enhancedcamera 11 | enhancedcamera.net 12 | v4.5.2 13 | 512 14 | 15 | 16 | 17 | true 18 | full 19 | false 20 | bin\Debug\ 21 | DEBUG;TRACE 22 | prompt 23 | 4 24 | 25 | 26 | pdbonly 27 | true 28 | bin\Release\ 29 | TRACE 30 | prompt 31 | 4 32 | 33 | 34 | 35 | False 36 | F:\Fivem\FiveM.app\citizen\clr2\lib\mono\4.5\CitizenFX.Core.dll 37 | 38 | 39 | F:\Fivem\FiveM.app\citizen\clr2\lib\mono\4.5\CitizenFX.Core.Client.dll 40 | 41 | 42 | False 43 | ..\dependencies\client\MenuAPI.dll 44 | 45 | 46 | ..\..\..\vStancer+sl_sh\fivem-vstancer\VStancer.Client\Newtonsoft.Json.dll 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | copy /y "$(TargetPath)" "D:\FiveM\ServerData\resources\enhancedcamera" 64 | copy /y "$(SolutionDir)EnhancedCamera\dist\__resource.lua" "D:\FiveM\ServerData\resources\enhancedcamera\__resource.lua" 65 | copy /y "$(SolutionDir)EnhancedCamera\dist\config.ini" "D:\FiveM\ServerData\resources\enhancedcamera\config.ini" 66 | copy /y "$(SolutionDir)dependencies\client\MenuAPI.dll" "D:\FiveM\ServerData\resources\enhancedcamera\MenuAPI.dll" 67 | copy /y "$(SolutionDir)dependencies\client\Newtonsoft.Json.dll" "D:\FiveM\ServerData\resources\enhancedcamera\Newtonsoft.Json.dll" 68 | 69 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015/2017 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # Visual Studio 2017 auto generated files 33 | Generated\ Files/ 34 | 35 | # MSTest test Results 36 | [Tt]est[Rr]esult*/ 37 | [Bb]uild[Ll]og.* 38 | 39 | # NUNIT 40 | *.VisualState.xml 41 | TestResult.xml 42 | 43 | # Build Results of an ATL Project 44 | [Dd]ebugPS/ 45 | [Rr]eleasePS/ 46 | dlldata.c 47 | 48 | # Benchmark Results 49 | BenchmarkDotNet.Artifacts/ 50 | 51 | # .NET Core 52 | project.lock.json 53 | project.fragment.lock.json 54 | artifacts/ 55 | **/Properties/launchSettings.json 56 | 57 | # StyleCop 58 | StyleCopReport.xml 59 | 60 | # Files built by Visual Studio 61 | *_i.c 62 | *_p.c 63 | *_i.h 64 | *.ilk 65 | *.meta 66 | *.obj 67 | *.iobj 68 | *.pch 69 | *.pdb 70 | *.ipdb 71 | *.pgc 72 | *.pgd 73 | *.rsp 74 | *.sbr 75 | *.tlb 76 | *.tli 77 | *.tlh 78 | *.tmp 79 | *.tmp_proj 80 | *.log 81 | *.vspscc 82 | *.vssscc 83 | .builds 84 | *.pidb 85 | *.svclog 86 | *.scc 87 | 88 | # Chutzpah Test files 89 | _Chutzpah* 90 | 91 | # Visual C++ cache files 92 | ipch/ 93 | *.aps 94 | *.ncb 95 | *.opendb 96 | *.opensdf 97 | *.sdf 98 | *.cachefile 99 | *.VC.db 100 | *.VC.VC.opendb 101 | 102 | # Visual Studio profiler 103 | *.psess 104 | *.vsp 105 | *.vspx 106 | *.sap 107 | 108 | # Visual Studio Trace Files 109 | *.e2e 110 | 111 | # TFS 2012 Local Workspace 112 | $tf/ 113 | 114 | # Guidance Automation Toolkit 115 | *.gpState 116 | 117 | # ReSharper is a .NET coding add-in 118 | _ReSharper*/ 119 | *.[Rr]e[Ss]harper 120 | *.DotSettings.user 121 | 122 | # JustCode is a .NET coding add-in 123 | .JustCode 124 | 125 | # TeamCity is a build add-in 126 | _TeamCity* 127 | 128 | # DotCover is a Code Coverage Tool 129 | *.dotCover 130 | 131 | # AxoCover is a Code Coverage Tool 132 | .axoCover/* 133 | !.axoCover/settings.json 134 | 135 | # Visual Studio code coverage results 136 | *.coverage 137 | *.coveragexml 138 | 139 | # NCrunch 140 | _NCrunch_* 141 | .*crunch*.local.xml 142 | nCrunchTemp_* 143 | 144 | # MightyMoose 145 | *.mm.* 146 | AutoTest.Net/ 147 | 148 | # Web workbench (sass) 149 | .sass-cache/ 150 | 151 | # Installshield output folder 152 | [Ee]xpress/ 153 | 154 | # DocProject is a documentation generator add-in 155 | DocProject/buildhelp/ 156 | DocProject/Help/*.HxT 157 | DocProject/Help/*.HxC 158 | DocProject/Help/*.hhc 159 | DocProject/Help/*.hhk 160 | DocProject/Help/*.hhp 161 | DocProject/Help/Html2 162 | DocProject/Help/html 163 | 164 | # Click-Once directory 165 | publish/ 166 | 167 | # Publish Web Output 168 | *.[Pp]ublish.xml 169 | *.azurePubxml 170 | # Note: Comment the next line if you want to checkin your web deploy settings, 171 | # but database connection strings (with potential passwords) will be unencrypted 172 | *.pubxml 173 | *.publishproj 174 | 175 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 176 | # checkin your Azure Web App publish settings, but sensitive information contained 177 | # in these scripts will be unencrypted 178 | PublishScripts/ 179 | 180 | # NuGet Packages 181 | *.nupkg 182 | # The packages folder can be ignored because of Package Restore 183 | **/[Pp]ackages/* 184 | # except build/, which is used as an MSBuild target. 185 | !**/[Pp]ackages/build/ 186 | # Uncomment if necessary however generally it will be regenerated when needed 187 | #!**/[Pp]ackages/repositories.config 188 | # NuGet v3's project.json files produces more ignorable files 189 | *.nuget.props 190 | *.nuget.targets 191 | 192 | # Microsoft Azure Build Output 193 | csx/ 194 | *.build.csdef 195 | 196 | # Microsoft Azure Emulator 197 | ecf/ 198 | rcf/ 199 | 200 | # Windows Store app package directories and files 201 | AppPackages/ 202 | BundleArtifacts/ 203 | Package.StoreAssociation.xml 204 | _pkginfo.txt 205 | *.appx 206 | 207 | # Visual Studio cache files 208 | # files ending in .cache can be ignored 209 | *.[Cc]ache 210 | # but keep track of directories ending in .cache 211 | !*.[Cc]ache/ 212 | 213 | # Others 214 | ClientBin/ 215 | ~$* 216 | *~ 217 | *.dbmdl 218 | *.dbproj.schemaview 219 | *.jfm 220 | *.pfx 221 | *.publishsettings 222 | orleans.codegen.cs 223 | 224 | # Including strong name files can present a security risk 225 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 226 | #*.snk 227 | 228 | # Since there are multiple workflows, uncomment next line to ignore bower_components 229 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 230 | #bower_components/ 231 | 232 | # RIA/Silverlight projects 233 | Generated_Code/ 234 | 235 | # Backup & report files from converting an old project file 236 | # to a newer Visual Studio version. Backup files are not needed, 237 | # because we have git ;-) 238 | _UpgradeReport_Files/ 239 | Backup*/ 240 | UpgradeLog*.XML 241 | UpgradeLog*.htm 242 | ServiceFabricBackup/ 243 | *.rptproj.bak 244 | 245 | # SQL Server files 246 | *.mdf 247 | *.ldf 248 | *.ndf 249 | 250 | # Business Intelligence projects 251 | *.rdl.data 252 | *.bim.layout 253 | *.bim_*.settings 254 | *.rptproj.rsuser 255 | 256 | # Microsoft Fakes 257 | FakesAssemblies/ 258 | 259 | # GhostDoc plugin setting file 260 | *.GhostDoc.xml 261 | 262 | # Node.js Tools for Visual Studio 263 | .ntvs_analysis.dat 264 | node_modules/ 265 | 266 | # Visual Studio 6 build log 267 | *.plg 268 | 269 | # Visual Studio 6 workspace options file 270 | *.opt 271 | 272 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 273 | *.vbw 274 | 275 | # Visual Studio LightSwitch build output 276 | **/*.HTMLClient/GeneratedArtifacts 277 | **/*.DesktopClient/GeneratedArtifacts 278 | **/*.DesktopClient/ModelManifest.xml 279 | **/*.Server/GeneratedArtifacts 280 | **/*.Server/ModelManifest.xml 281 | _Pvt_Extensions 282 | 283 | # Paket dependency manager 284 | .paket/paket.exe 285 | paket-files/ 286 | 287 | # FAKE - F# Make 288 | .fake/ 289 | 290 | # JetBrains Rider 291 | .idea/ 292 | *.sln.iml 293 | 294 | # CodeRush 295 | .cr/ 296 | 297 | # Python Tools for Visual Studio (PTVS) 298 | __pycache__/ 299 | *.pyc 300 | 301 | # Cake - Uncomment if you are using it 302 | # tools/** 303 | # !tools/packages.config 304 | 305 | # Tabs Studio 306 | *.tss 307 | 308 | # Telerik's JustMock configuration file 309 | *.jmconfig 310 | 311 | # BizTalk build output 312 | *.btp.cs 313 | *.btm.cs 314 | *.odx.cs 315 | *.xsd.cs 316 | 317 | # OpenCover UI analysis results 318 | OpenCover/ 319 | 320 | # Azure Stream Analytics local run output 321 | ASALocalRun/ 322 | 323 | # MSBuild Binary and Structured Log 324 | *.binlog 325 | 326 | # NVidia Nsight GPU debugger configuration file 327 | *.nvuser 328 | 329 | # MFractors (Xamarin productivity tool) working folder 330 | .mfractor/ 331 | -------------------------------------------------------------------------------- /EnhancedCamera/langs/zh_CN.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace CustomCamera.langs { 9 | class zh_CN { 10 | public Hashtable data() { 11 | 12 | Hashtable ht = new Hashtable(); 13 | 14 | // Main Menu 15 | ht.Add("MAIN_MENU_TITLE", "增强型游戏镜头"); 16 | ht.Add("MAIN_MENU_DESC", "高级镜头和无人机镜头设置"); 17 | ht.Add("MAIN_MENU_ENABLE_LEAD", "启用漂移镜头"); 18 | ht.Add("MAIN_MENU_ENABLE_CHASE", "启用追踪镜头"); 19 | ht.Add("MAIN_MENU_ENABLE_DRONE", "启用无人机镜头"); 20 | ht.Add("MAIN_MENU_ENABLE_LEAD_DESC", "主镜头,行为取决于汽车的漂移角度和速度"); 21 | ht.Add("MAIN_MENU_ENABLE_CHASE_DESC", "锁定前方目标,如果目标不在范围内,则切换到普通镜头"); 22 | ht.Add("MAIN_MENU_ENABLE_DRONE_DESC", "自由无人机镜头,提供不同的模式,例如穿梭机和无重力飞行"); 23 | ht.Add("MAIN_MENU_LEAD_CHASE_CONF", "漂移和追踪镜头设定"); 24 | ht.Add("MAIN_MENU_DRONE_CONF", "无人机镜头设定"); 25 | ht.Add("MAIN_MENU_LEAD_CHASE_CONF_DESC", "修改漂移或追踪镜头的参数设定"); 26 | ht.Add("MAIN_MENU_DRONE_CONF_DESC", "修改无人机镜头的参数设定"); 27 | ht.Add("MAIN_MENU_CREDITS", "鸣谢"); 28 | ht.Add("MAIN_MENU_CREDITS_DESC", "~g~Shrimp~s~ - 提供想法并实现\n" + 29 | "~g~Tom Grobbe~s~ - MenuAPI 界面库的作者,以及保存和加载设定的代码\n" + 30 | "~g~QuadrupleTurbo~s~ - 提供想法并帮助测试\n" + 31 | "~y~No Name Drift~s~ 和 ~y~Velocity~s~ 漂移服务器进行游戏测试和反馈\n" + 32 | "~y~ZeroDream~s~ 增加多语言支持以及中文翻译\n"); 33 | 34 | // Custom Camera 35 | ht.Add("CUSTOM_CAM_MANAGE", "管理镜头"); 36 | ht.Add("CUSTOM_CAM_MANAGE_DESC", "管理此已保存的镜头"); 37 | ht.Add("CUSTOM_CAM_TITLE", "自定义摄像机"); 38 | ht.Add("CUSTOM_CAM_DESC", "漂移和追踪镜头设定"); 39 | ht.Add("CUSTOM_CAM_LOCK_POSITION", "锁定位置偏移"); 40 | ht.Add("CUSTOM_CAM_LOCK_POSITION_DESC", "锁定镜头的位置,在用于固定镜头和第一人称镜头时非常有用"); 41 | ht.Add("CUSTOM_CAM_LINEAR", "线性位置偏移"); 42 | ht.Add("CUSTOM_CAM_LINEAR_DESC", "镜头不是围绕车辆做圆周运动,而是沿着汽车的 X 轴移动,适合拍电影"); 43 | ht.Add("CUSTOM_CAM_LOCK_ROTATE", "锁定镜头的水平旋转"); 44 | ht.Add("CUSTOM_CAM_LOCK_ROTATE_DESC", "修改了镜头围绕车辆旋转的方式"); 45 | ht.Add("CUSTOM_CAM_MODIFIER", "修改器"); 46 | ht.Add("CUSTOM_CAM_MODIFIER_DESC", "这个修改器 * 角速度 = 旋转目标,数值越高,则相机越偏离锁定值 (-1,1)"); 47 | ht.Add("CUSTOM_CAM_YAW", "偏航插值"); 48 | ht.Add("CUSTOM_CAM_YAW_DESC", "数值越低,运动更平稳。警告:追踪镜头的设定是相反的,0 为最大值,1 为完全锁定 (0,1)"); 49 | ht.Add("CUSTOM_CAM_ROLL", "滚动插值"); 50 | ht.Add("CUSTOM_CAM_ROLL_DESC", "数值越低,运动更平稳 (0,1)"); 51 | ht.Add("CUSTOM_CAM_PITCH", "俯仰插值"); 52 | ht.Add("CUSTOM_CAM_PITCH_DESC", "数值越低,运动更平稳 (0,1)"); 53 | ht.Add("CUSTOM_CAM_OFFSET", "镜头偏移量"); 54 | ht.Add("CUSTOM_CAM_OFFSET_DESC", "设定追踪镜头向目标移动的速度矢量偏移 (0,5)"); 55 | ht.Add("CUSTOM_CAM_POSITION", "位置插值"); 56 | ht.Add("CUSTOM_CAM_POSITION_DESC", "数值越低,运动更平稳,但是延迟更高 (0,1)"); 57 | ht.Add("CUSTOM_CAM_FOV", "FOV 视场"); 58 | ht.Add("CUSTOM_CAM_FOV_DESC", "修改镜头的视场 (20,120)"); 59 | ht.Add("CUSTOM_CAM_Y_OFFSET", "Y 偏移"); 60 | ht.Add("CUSTOM_CAM_Y_OFFSET_DESC", "修改镜头向前的偏移量 (-8,8)"); 61 | ht.Add("CUSTOM_CAM_X_OFFSET", "X 偏移"); 62 | ht.Add("CUSTOM_CAM_X_OFFSET_DESC", "修改相机左右的偏移量 (-5,8)"); 63 | ht.Add("CUSTOM_CAM_Z_OFFSET", "Z 偏移"); 64 | ht.Add("CUSTOM_CAM_Z_OFFSET_DESC", "修改相机上下的偏移量 (-5,8)"); 65 | ht.Add("CUSTOM_CAM_MAX_ANGLE", "最大锁定角度"); 66 | ht.Add("CUSTOM_CAM_MAX_ANGLE_DESC", "保持锁定的速度矢量最大角度,超过此角度后将会切换回普通镜头"); 67 | ht.Add("CUSTOM_CAM_PRESETS", "预设"); 68 | ht.Add("CUSTOM_CAM_PRESETS_DESC", "应用镜头预设"); 69 | ht.Add("CUSTOM_CAM_PRESETS_TANDEM", "追走镜头 1.0"); 70 | ht.Add("CUSTOM_CAM_PRESETS_TANDEM_DESC", "适用于漂移追走时使用的镜头"); 71 | ht.Add("CUSTOM_CAM_PRESETS_FPV", "FPV 第一人称"); 72 | ht.Add("CUSTOM_CAM_PRESETS_FPV_DESC", "适用于第一人称漂移时使用的镜头"); 73 | ht.Add("CUSTOM_CAM_PRESETS_NFS", "NFS 镜头"); 74 | ht.Add("CUSTOM_CAM_PRESETS_NFS_DESC", "街机游戏一样的画面体验"); 75 | ht.Add("CUSTOM_CAM_PRESETS_MENU", "预设菜单"); 76 | ht.Add("CUSTOM_CAM_PRESETS_MENU_DESC", "从这里开始"); 77 | ht.Add("CUSTOM_CAM_SAVED", "保存的镜头"); 78 | ht.Add("CUSTOM_CAM_SAVED_DESC", "用户创建的镜头"); 79 | ht.Add("CUSTOM_CAM_SAVE_CURRENT", "保存当前镜头"); 80 | ht.Add("CUSTOM_CAM_SAVE_CURRENT_DESC", "将当前的镜头设定储存为预设"); 81 | ht.Add("CUSTOM_CAM_SPAWN", "应用镜头预设"); 82 | ht.Add("CUSTOM_CAM_SPAWN_DESC", "应用此保存的镜头预设"); 83 | ht.Add("CUSTOM_CAM_RENAME", "重命名预设"); 84 | ht.Add("CUSTOM_CAM_RENAME_DESC", "重命名你已保存的镜头预设"); 85 | ht.Add("CUSTOM_CAM_DELETE", "~r~删除预设"); 86 | ht.Add("CUSTOM_CAM_DELETE_DESC", "~r~这将会删除此预设。警告:这是不可撤销的操作"); 87 | 88 | // Drones Menu 89 | ht.Add("DRONE_MANAGE_TITLE", "管理无人机"); 90 | ht.Add("DRONE_MANAGE_DESC", "管理已保存的无人机设定"); 91 | ht.Add("DRONE_TITLE", "无人机"); 92 | ht.Add("DRONE_DESC", "无人机镜头参数设定"); 93 | ht.Add("DRONE_RACE", "穿梭机"); 94 | ht.Add("DRONE_ZERO_G", "无重力"); 95 | ht.Add("DRONE_SPECTATOR", "旁观者"); 96 | ht.Add("DRONE_HOMING", "返航无人机"); 97 | ht.Add("DRONE_INVERT_PITCH", "反向俯仰"); 98 | ht.Add("DRONE_INVERT_PITCH_DESC", "调换俯仰控制的按键"); 99 | ht.Add("DRONE_INVERT_ROLL", "反向滚动"); 100 | ht.Add("DRONE_INVERT_ROLL_DESC", "调换滚动控制的按键"); 101 | ht.Add("DRONE_GRAVITY", "重力倍数"); 102 | ht.Add("DRONE_GRAVITY_DESC", "修改重力常数,更高的值会使无人机在自由落体运动中下降得更快"); 103 | ht.Add("DRONE_TIMESTEP", "时间倍数"); 104 | ht.Add("DRONE_TIMESTEP_DESC", "影响重力和无人机的响应能力"); 105 | ht.Add("DRONE_DRAG", "阻力倍数"); 106 | ht.Add("DRONE_DRAG_DESC", "设定空气阻力,越高的值会使无人机更快地失去速度"); 107 | ht.Add("DRONE_ACCELE", "加速倍数"); 108 | ht.Add("DRONE_ACCELE_DESC", "设定无人机的加速性能"); 109 | ht.Add("DRONE_PITCH", "俯仰倍数"); 110 | ht.Add("DRONE_PITCH_DESC", "设定无人机的俯仰响应速度 (pitch)."); 111 | ht.Add("DRONE_ROLL", "滚动倍数"); 112 | ht.Add("DRONE_ROLL_DESC", "设定无人机的滚动响应速度 (roll)."); 113 | ht.Add("DRONE_YAW", "偏航倍数"); 114 | ht.Add("DRONE_YAW_DESC", "设定无人机的偏航响应速度 (yaw)."); 115 | ht.Add("DRONE_TILT", "倾斜角度"); 116 | ht.Add("DRONE_TILT_DESC", "设定镜头相对于无人机的倾斜角度"); 117 | ht.Add("DRONE_FOV", "FOV 视场"); 118 | ht.Add("DRONE_FOV_DESC", "设定镜头的视场"); 119 | ht.Add("DRONE_MAX_VELOCITY", "最高速度"); 120 | ht.Add("DRONE_MAX_VELOCITY_DESC", "设定无人机的最高速度"); 121 | ht.Add("DRONE_SAVED", "已保存的无人机"); 122 | ht.Add("DRONE_SAVED_DESC", "用户创建的无人机"); 123 | ht.Add("DRONE_SAVED_TITLE", "已保存的无人机"); 124 | ht.Add("DRONE_SAVE_CURRENT", "保存当前无人机"); 125 | ht.Add("DRONE_SAVE_CURRENT_DESC", "保存当前的无人机参数"); 126 | ht.Add("DRONE_SPAWN", "应用无人机参数"); 127 | ht.Add("DRONE_SPAWN_DESC", "应用此已保存的无人机参数"); 128 | ht.Add("DRONE_RENAME", "重命名无人机"); 129 | ht.Add("DRONE_RENAME_DESC", "重命名你已保存的无人机"); 130 | ht.Add("DRONE_DELETE", "~r~删除无人机"); 131 | ht.Add("DRONE_DELETE_DESC", "~r~这将会删除你已保存的无人机。警告:这是不可撤销的操作"); 132 | ht.Add("DRONE_MODE", "模式"); 133 | ht.Add("DRONE_MODE_DESC", "选择无人机的飞行模式\n" + 134 | "~r~Race 穿梭机~s~ - 普通,带有重力的无人机\n" + 135 | "~g~Zero-G 无重力~s~ - 没有重力的无人机,增加了减速\n" + 136 | "~b~Spectator 旁观者~s~ - 方便操作进行旁观\n" + 137 | "~y~Homing 返航机~s~ - 获取目标并将其作为中心点"); 138 | return ht; 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /EnhancedCamera/langs/zh_TW.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace CustomCamera.langs { 9 | class zh_TW { 10 | public Hashtable data() { 11 | 12 | Hashtable ht = new Hashtable(); 13 | 14 | // Main Menu 15 | ht.Add("MAIN_MENU_TITLE", "增強型遊戲鏡頭"); 16 | ht.Add("MAIN_MENU_DESC", "高級鏡頭和無人機鏡頭設置"); 17 | ht.Add("MAIN_MENU_ENABLE_LEAD", "啟用漂移鏡頭"); 18 | ht.Add("MAIN_MENU_ENABLE_CHASE", "啟用追踪鏡頭"); 19 | ht.Add("MAIN_MENU_ENABLE_DRONE", "啟用無人機鏡頭"); 20 | ht.Add("MAIN_MENU_ENABLE_LEAD_DESC", "主鏡頭,行為取決於汽車的漂移角度和速度"); 21 | ht.Add("MAIN_MENU_ENABLE_CHASE_DESC", "鎖定前方目標,如果目標不在範圍內,則切換到普通鏡頭"); 22 | ht.Add("MAIN_MENU_ENABLE_DRONE_DESC", "自由無人機鏡頭,提供不同的模式,例如穿梭機和無重力飛行"); 23 | ht.Add("MAIN_MENU_LEAD_CHASE_CONF", "漂移和追踪鏡頭設定"); 24 | ht.Add("MAIN_MENU_DRONE_CONF", "無人機鏡頭設定"); 25 | ht.Add("MAIN_MENU_LEAD_CHASE_CONF_DESC", "修改漂移或追踪鏡頭的參數設定"); 26 | ht.Add("MAIN_MENU_DRONE_CONF_DESC", "修改無人機鏡頭的參數設定"); 27 | ht.Add("MAIN_MENU_CREDITS", "鳴謝"); 28 | ht.Add("MAIN_MENU_CREDITS_DESC", "~g~Shrimp~s~ - 提供想法並實現\n" + 29 | "~g~Tom Grobbe~s~ - MenuAPI 界面庫的作者,以及保存和加載設定的代碼\n" + 30 | "~g~QuadrupleTurbo~s~ - 提供想法並幫助測試\n" + 31 | "~y~No Name Drift~s~ 和 ~y~Velocity~s~ 漂移服務器進行遊戲測試和反饋\n" + 32 | "~y~ZeroDream~s~ 增加多語言支持以及中文翻譯\n"); 33 | 34 | // Custom Camera 35 | ht.Add("CUSTOM_CAM_MANAGE", "管理鏡頭"); 36 | ht.Add("CUSTOM_CAM_MANAGE_DESC", "管理此已保存的鏡頭"); 37 | ht.Add("CUSTOM_CAM_TITLE", "自定義攝像機"); 38 | ht.Add("CUSTOM_CAM_DESC", "漂移和追踪鏡頭設定"); 39 | ht.Add("CUSTOM_CAM_LOCK_POSITION", "鎖定位置偏移"); 40 | ht.Add("CUSTOM_CAM_LOCK_POSITION_DESC", "鎖定鏡頭的位置,在用於固定鏡頭和第一人稱鏡頭時非常有用"); 41 | ht.Add("CUSTOM_CAM_LINEAR", "線性位置偏移"); 42 | ht.Add("CUSTOM_CAM_LINEAR_DESC", "鏡頭不是圍繞車輛做圓周運動,而是沿著汽車的 X 軸移動,適合拍電影"); 43 | ht.Add("CUSTOM_CAM_LOCK_ROTATE", "鎖定鏡頭的水平旋轉"); 44 | ht.Add("CUSTOM_CAM_LOCK_ROTATE_DESC", "修改了鏡頭圍繞車輛旋轉的方式"); 45 | ht.Add("CUSTOM_CAM_MODIFIER", "修改器"); 46 | ht.Add("CUSTOM_CAM_MODIFIER_DESC", "這個修改器 * 角速度 = 旋轉目標,數值越高,則相機越偏離鎖定值 (-1,1)"); 47 | ht.Add("CUSTOM_CAM_YAW", "偏航插值"); 48 | ht.Add("CUSTOM_CAM_YAW_DESC", "數值越低,運動更平穩。警告:追踪鏡頭的設​​定是相反的,0 為最大值,1 為完全鎖定 (0,1)"); 49 | ht.Add("CUSTOM_CAM_ROLL", "滾動插值"); 50 | ht.Add("CUSTOM_CAM_ROLL_DESC", "數值越低,運動更平穩 (0,1)"); 51 | ht.Add("CUSTOM_CAM_PITCH", "俯仰插值"); 52 | ht.Add("CUSTOM_CAM_PITCH_DESC", "數值越低,運動更平穩 (0,1)"); 53 | ht.Add("CUSTOM_CAM_OFFSET", "鏡頭偏移量"); 54 | ht.Add("CUSTOM_CAM_OFFSET_DESC", "設定追踪鏡頭向目標移動的速度矢量偏移 (0,5)"); 55 | ht.Add("CUSTOM_CAM_POSITION", "位置插值"); 56 | ht.Add("CUSTOM_CAM_POSITION_DESC", "數值越低,運動更平穩,但是延遲更高 (0,1)"); 57 | ht.Add("CUSTOM_CAM_FOV", "FOV 視場"); 58 | ht.Add("CUSTOM_CAM_FOV_DESC", "修改鏡頭的視場 (20,120)"); 59 | ht.Add("CUSTOM_CAM_Y_OFFSET", "Y 偏移"); 60 | ht.Add("CUSTOM_CAM_Y_OFFSET_DESC", "修改鏡頭向前的偏移量 (-8,8)"); 61 | ht.Add("CUSTOM_CAM_X_OFFSET", "X 偏移"); 62 | ht.Add("CUSTOM_CAM_X_OFFSET_DESC", "修改相機左右的偏移量 (-5,8)"); 63 | ht.Add("CUSTOM_CAM_Z_OFFSET", "Z 偏移"); 64 | ht.Add("CUSTOM_CAM_Z_OFFSET_DESC", "修改相機上下的偏移量 (-5,8)"); 65 | ht.Add("CUSTOM_CAM_MAX_ANGLE", "最大鎖定角度"); 66 | ht.Add("CUSTOM_CAM_MAX_ANGLE_DESC", "保持鎖定的速度矢量最大角度,超過此角度後將會切換回普通鏡頭"); 67 | ht.Add("CUSTOM_CAM_PRESETS", "預設"); 68 | ht.Add("CUSTOM_CAM_PRESETS_DESC", "應用鏡頭預設"); 69 | ht.Add("CUSTOM_CAM_PRESETS_TANDEM", "追走鏡頭 1.0"); 70 | ht.Add("CUSTOM_CAM_PRESETS_TANDEM_DESC", "適用於漂移追走時使用的鏡頭"); 71 | ht.Add("CUSTOM_CAM_PRESETS_FPV", "FPV 第一人稱"); 72 | ht.Add("CUSTOM_CAM_PRESETS_FPV_DESC", "適用於第一人稱漂移時使用的鏡頭"); 73 | ht.Add("CUSTOM_CAM_PRESETS_NFS", "NFS 鏡頭"); 74 | ht.Add("CUSTOM_CAM_PRESETS_NFS_DESC", "街機遊戲一樣的畫面體驗"); 75 | ht.Add("CUSTOM_CAM_PRESETS_MENU", "預設菜單"); 76 | ht.Add("CUSTOM_CAM_PRESETS_MENU_DESC", "從這裡開始"); 77 | ht.Add("CUSTOM_CAM_SAVED", "保存的鏡頭"); 78 | ht.Add("CUSTOM_CAM_SAVED_DESC", "用戶創建的鏡頭"); 79 | ht.Add("CUSTOM_CAM_SAVE_CURRENT", "保存當前鏡頭"); 80 | ht.Add("CUSTOM_CAM_SAVE_CURRENT_DESC", "將當前的鏡頭設定儲存為預設"); 81 | ht.Add("CUSTOM_CAM_SPAWN", "應用鏡頭預設"); 82 | ht.Add("CUSTOM_CAM_SPAWN_DESC", "應用此保存的鏡頭預設"); 83 | ht.Add("CUSTOM_CAM_RENAME", "重命名預設"); 84 | ht.Add("CUSTOM_CAM_RENAME_DESC", "重命名你已保存的鏡頭預設"); 85 | ht.Add("CUSTOM_CAM_DELETE", "~r~刪除預設"); 86 | ht.Add("CUSTOM_CAM_DELETE_DESC", "~r~這將會刪除此預設。警告:這是不可撤銷的操作"); 87 | 88 | // Drones Menu 89 | ht.Add("DRONE_MANAGE_TITLE", "管理無人機"); 90 | ht.Add("DRONE_MANAGE_DESC", "管理已保存的無人機設定"); 91 | ht.Add("DRONE_TITLE", "無人機"); 92 | ht.Add("DRONE_DESC", "無人機鏡頭參數設定"); 93 | ht.Add("DRONE_RACE", "穿梭機"); 94 | ht.Add("DRONE_ZERO_G", "無重力"); 95 | ht.Add("DRONE_SPECTATOR", "旁觀者"); 96 | ht.Add("DRONE_HOMING", "返航無人機"); 97 | ht.Add("DRONE_INVERT_PITCH", "反向俯仰"); 98 | ht.Add("DRONE_INVERT_PITCH_DESC", "調換俯仰控制的按鍵"); 99 | ht.Add("DRONE_INVERT_ROLL", "反向滾動"); 100 | ht.Add("DRONE_INVERT_ROLL_DESC", "調換滾動控制的按鍵"); 101 | ht.Add("DRONE_GRAVITY", "重力倍數"); 102 | ht.Add("DRONE_GRAVITY_DESC", "修改重力常數,更高的值會使無人機在自由落體運動中下降得更快"); 103 | ht.Add("DRONE_TIMESTEP", "時間倍數"); 104 | ht.Add("DRONE_TIMESTEP_DESC", "影響重力和無人機的響應能力"); 105 | ht.Add("DRONE_DRAG", "阻力倍數"); 106 | ht.Add("DRONE_DRAG_DESC", "設定空氣阻力,越高的值會使無人機更快地失去速度"); 107 | ht.Add("DRONE_ACCELE", "加速倍數"); 108 | ht.Add("DRONE_ACCELE_DESC", "設定無人機的加速性能"); 109 | ht.Add("DRONE_PITCH", "俯仰倍數"); 110 | ht.Add("DRONE_PITCH_DESC", "設定無人機的俯仰響應速度 (pitch)."); 111 | ht.Add("DRONE_ROLL", "滾動倍數"); 112 | ht.Add("DRONE_ROLL_DESC", "設定無人機的滾動響應速度 (roll)."); 113 | ht.Add("DRONE_YAW", "偏航倍數"); 114 | ht.Add("DRONE_YAW_DESC", "設定無人機的偏航響應速度 (yaw)."); 115 | ht.Add("DRONE_TILT", "傾斜角度"); 116 | ht.Add("DRONE_TILT_DESC", "設定鏡頭相對於無人機的傾斜角度"); 117 | ht.Add("DRONE_FOV", "FOV 視場"); 118 | ht.Add("DRONE_FOV_DESC", "設定鏡頭的視場"); 119 | ht.Add("DRONE_MAX_VELOCITY", "最高速度"); 120 | ht.Add("DRONE_MAX_VELOCITY_DESC", "設定無人機的最高速度"); 121 | ht.Add("DRONE_SAVED", "已保存的無人機"); 122 | ht.Add("DRONE_SAVED_DESC", "用戶創建的無人機"); 123 | ht.Add("DRONE_SAVED_TITLE", "已保存的無人機"); 124 | ht.Add("DRONE_SAVE_CURRENT", "保存當前無人機"); 125 | ht.Add("DRONE_SAVE_CURRENT_DESC", "保存當前的無人機參數"); 126 | ht.Add("DRONE_SPAWN", "應用無人機參數"); 127 | ht.Add("DRONE_SPAWN_DESC", "應用此已保存的無人機參數"); 128 | ht.Add("DRONE_RENAME", "重命名無人機"); 129 | ht.Add("DRONE_RENAME_DESC", "重命名你已保存的無人機"); 130 | ht.Add("DRONE_DELETE", "~r~刪除無人機"); 131 | ht.Add("DRONE_DELETE_DESC", "~r~這將會刪除你已保存的無人機。警告:這是不可撤銷的操作"); 132 | ht.Add("DRONE_MODE", "模式"); 133 | ht.Add("DRONE_MODE_DESC", "選擇無人機的飛行模式\n" + 134 | "~r~Race 穿梭機~s~ - 普通,帶有重力的無人機\n" + 135 | "~g~Zero-G 無重力~s~ - 沒有重力的無人機,增加了減速\n" + 136 | "~b~Spectator 旁觀者~s~ - 方便操作進行旁觀\n" + 137 | "~y~Homing 返航機~s~ - 獲取目標並將其作為中心點"); 138 | return ht; 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /EnhancedCamera/langs/en_US.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace CustomCamera.langs { 9 | class en_US { 10 | public Hashtable data() { 11 | 12 | Hashtable ht = new Hashtable(); 13 | 14 | // Main Menu 15 | ht.Add("MAIN_MENU_TITLE", "Enhanced camera"); 16 | ht.Add("MAIN_MENU_DESC", "Lead, chase and drone camera options"); 17 | ht.Add("MAIN_MENU_ENABLE_LEAD", "Enable lead camera"); 18 | ht.Add("MAIN_MENU_ENABLE_CHASE", "Enable chase camera"); 19 | ht.Add("MAIN_MENU_ENABLE_DRONE", "Enable drone camera"); 20 | ht.Add("MAIN_MENU_ENABLE_LEAD_DESC", "Main camera, behaviour dependant on angular velocity of the car."); 21 | ht.Add("MAIN_MENU_ENABLE_CHASE_DESC", "Locks to a target in front, switches to regular cam if target not in range."); 22 | ht.Add("MAIN_MENU_ENABLE_DRONE_DESC", "Free drone camera to spectate/fly around. Different modes available."); 23 | ht.Add("MAIN_MENU_LEAD_CHASE_CONF", "Lead/chase cam parameters"); 24 | ht.Add("MAIN_MENU_DRONE_CONF", "Drone cam parameters"); 25 | ht.Add("MAIN_MENU_LEAD_CHASE_CONF_DESC", "Tune parameters for lead and chase camera"); 26 | ht.Add("MAIN_MENU_DRONE_CONF_DESC", "Tune parameters for drone camera"); 27 | ht.Add("MAIN_MENU_CREDITS", "Credits"); 28 | ht.Add("MAIN_MENU_CREDITS_DESC", "~g~Shrimp~s~ - idea and execution\n" + 29 | "~g~Tom Grobbe~s~ - MenuAPI used for GUI, code snippets for saving/loading\n" + 30 | "~g~QuadrupleTurbo~s~ - Help with ideas and testing\n" + 31 | "~y~No Name Drift~s~ and ~y~Velocity~s~ drift servers - playtesting and feedback\n" + 32 | "~r~ZeroDream~s~ - Implement menu localization and Chinese translate\n"); 33 | 34 | // Custom Camera 35 | ht.Add("CUSTOM_CAM_MANAGE", "Manage Camera"); 36 | ht.Add("CUSTOM_CAM_MANAGE_DESC", "Manage this saved camera."); 37 | ht.Add("CUSTOM_CAM_TITLE", "Enhanced camera"); 38 | ht.Add("CUSTOM_CAM_DESC", "Lead/chase camera parameters"); 39 | ht.Add("CUSTOM_CAM_LOCK_POSITION", "Lock position offset"); 40 | ht.Add("CUSTOM_CAM_LOCK_POSITION_DESC", "Locks position offset, useful when sticking camera to the car - on top of hood, as FPV cam, etc."); 41 | ht.Add("CUSTOM_CAM_LINEAR", "Linear position offset"); 42 | ht.Add("CUSTOM_CAM_LINEAR_DESC", "Instead of circular motion around the car, the camera moves along car's X axis. Dope for cinematic shots."); 43 | ht.Add("CUSTOM_CAM_LOCK_ROTATE", "Lock rotation to camera plane"); 44 | ht.Add("CUSTOM_CAM_LOCK_ROTATE_DESC", "Changes the way that camera rotates around car (mostly visible on uneven ground)."); 45 | ht.Add("CUSTOM_CAM_MODIFIER", "Modifier"); 46 | ht.Add("CUSTOM_CAM_MODIFIER_DESC", "This modifier * angular velocity = target rotation. Higher values make camera move further from lock. (-1,1)"); 47 | ht.Add("CUSTOM_CAM_YAW", "Yaw interpolation"); 48 | ht.Add("CUSTOM_CAM_YAW_DESC", "Lower values - smoother movement. WARNING: Slider is inversed for chase camera - 0 is max interpolation, 1 is complete lock. (0,1)"); 49 | ht.Add("CUSTOM_CAM_ROLL", "Roll interpolation"); 50 | ht.Add("CUSTOM_CAM_ROLL_DESC", "Lower values - smoother movement. (0,1)"); 51 | ht.Add("CUSTOM_CAM_PITCH", "Pitch interpolation"); 52 | ht.Add("CUSTOM_CAM_PITCH_DESC", "Lower values - smoother movement. (0,1)"); 53 | ht.Add("CUSTOM_CAM_OFFSET", "Camera offset"); 54 | ht.Add("CUSTOM_CAM_OFFSET_DESC", "Offsets chase camera target towards its velocity vector. (0,5)"); 55 | ht.Add("CUSTOM_CAM_POSITION", "Position interpolation"); 56 | ht.Add("CUSTOM_CAM_POSITION_DESC", "Lower values - smoother movement, higher delay. (0,1)"); 57 | ht.Add("CUSTOM_CAM_FOV", "FOV"); 58 | ht.Add("CUSTOM_CAM_FOV_DESC", "Change custom camera's FOV. (20,120)"); 59 | ht.Add("CUSTOM_CAM_Y_OFFSET", "Y offset"); 60 | ht.Add("CUSTOM_CAM_Y_OFFSET_DESC", "Custom camera offset in forward direction. (-8,8)"); 61 | ht.Add("CUSTOM_CAM_X_OFFSET", "X offset"); 62 | ht.Add("CUSTOM_CAM_X_OFFSET_DESC", "Custom camera offset in side direction. (-5,8)"); 63 | ht.Add("CUSTOM_CAM_Z_OFFSET", "Z offset"); 64 | ht.Add("CUSTOM_CAM_Z_OFFSET_DESC", "Custom camera offset in up direction. (-5,8)"); 65 | ht.Add("CUSTOM_CAM_MAX_ANGLE", "Max angle to lock."); 66 | ht.Add("CUSTOM_CAM_MAX_ANGLE_DESC", "Max angle from velocity vector to keep the lock on, if angle exceeds this limit, camera switches back to normal. (25,360)"); 67 | ht.Add("CUSTOM_CAM_PRESETS", "Presets"); 68 | ht.Add("CUSTOM_CAM_PRESETS_DESC", "Spawn camera presets"); 69 | ht.Add("CUSTOM_CAM_PRESETS_TANDEM", "Tandem Camera 1.0"); 70 | ht.Add("CUSTOM_CAM_PRESETS_TANDEM_DESC", "Chef's specialty"); 71 | ht.Add("CUSTOM_CAM_PRESETS_FPV", "FPV camera base"); 72 | ht.Add("CUSTOM_CAM_PRESETS_FPV_DESC", "Best to use with chicken model"); 73 | ht.Add("CUSTOM_CAM_PRESETS_NFS", "NFS camera"); 74 | ht.Add("CUSTOM_CAM_PRESETS_NFS_DESC", "Arcade game experience"); 75 | ht.Add("CUSTOM_CAM_PRESETS_MENU", "Presets menu"); 76 | ht.Add("CUSTOM_CAM_PRESETS_MENU_DESC", "Get started here"); 77 | ht.Add("CUSTOM_CAM_SAVED", "Saved cameras"); 78 | ht.Add("CUSTOM_CAM_SAVED_DESC", "User created cameras"); 79 | ht.Add("CUSTOM_CAM_SAVE_CURRENT", "Save Current Camera"); 80 | ht.Add("CUSTOM_CAM_SAVE_CURRENT_DESC", "Save the current camera."); 81 | ht.Add("CUSTOM_CAM_SPAWN", "Spawn Camera"); 82 | ht.Add("CUSTOM_CAM_SPAWN_DESC", "Spawn this saved camera."); 83 | ht.Add("CUSTOM_CAM_RENAME", "Rename Camera"); 84 | ht.Add("CUSTOM_CAM_RENAME_DESC", "Rename your saved camera."); 85 | ht.Add("CUSTOM_CAM_DELETE", "~r~Delete Camera"); 86 | ht.Add("CUSTOM_CAM_DELETE_DESC", "~r~This will delete your saved camera. Warning: this can NOT be undone!"); 87 | 88 | // Drones Menu 89 | ht.Add("DRONE_MANAGE_TITLE", "Manage Drone"); 90 | ht.Add("DRONE_MANAGE_DESC", "Manage this saved drone parameters."); 91 | ht.Add("DRONE_TITLE", "Enhanced camera"); 92 | ht.Add("DRONE_DESC", "Drone Camera parameters"); 93 | ht.Add("DRONE_RACE", "Race drone"); 94 | ht.Add("DRONE_ZERO_G", "Zero-G drone"); 95 | ht.Add("DRONE_SPECTATOR", "Spectator drone"); 96 | ht.Add("DRONE_HOMING", "Homing drone"); 97 | ht.Add("DRONE_INVERT_PITCH", "Invert pitch"); 98 | ht.Add("DRONE_INVERT_PITCH_DESC", "Inverts user input in pitch axis."); 99 | ht.Add("DRONE_INVERT_ROLL", "Invert roll"); 100 | ht.Add("DRONE_INVERT_ROLL_DESC", "Inverts user input in roll axis."); 101 | ht.Add("DRONE_GRAVITY", "Gravity multiplier"); 102 | ht.Add("DRONE_GRAVITY_DESC", "Modifies gravity constant, higher values makes drone fall quicker during freefall."); 103 | ht.Add("DRONE_TIMESTEP", "Timestep multiplier"); 104 | ht.Add("DRONE_TIMESTEP_DESC", "Affects gravity and drone responsiveness."); 105 | ht.Add("DRONE_DRAG", "Drag multiplier"); 106 | ht.Add("DRONE_DRAG_DESC", "How much air ressistance there is - higher values make drone lose velocity quicker."); 107 | ht.Add("DRONE_ACCELE", "Acceleration multiplier"); 108 | ht.Add("DRONE_ACCELE_DESC", "How responsive drone is in terms of acceleration."); 109 | ht.Add("DRONE_PITCH", "Pitch multiplier"); 110 | ht.Add("DRONE_PITCH_DESC", "How responsive drone is in terms of rotation (pitch)."); 111 | ht.Add("DRONE_ROLL", "Roll multiplier"); 112 | ht.Add("DRONE_ROLL_DESC", "How responsive drone is in terms of rotation (roll)."); 113 | ht.Add("DRONE_YAW", "Yaw multiplier"); 114 | ht.Add("DRONE_YAW_DESC", "How responsive drone is in terms of rotation (yaw)."); 115 | ht.Add("DRONE_TILT", "Tilt angle"); 116 | ht.Add("DRONE_TILT_DESC", "Defines how much is camera tilted relative to the drone."); 117 | ht.Add("DRONE_FOV", "FOV"); 118 | ht.Add("DRONE_FOV_DESC", "Field of view of the camera"); 119 | ht.Add("DRONE_MAX_VELOCITY", "Max velocity"); 120 | ht.Add("DRONE_MAX_VELOCITY_DESC", "Max velocity of the drone"); 121 | ht.Add("DRONE_SAVED", "Saved drones"); 122 | ht.Add("DRONE_SAVED_DESC", "User created drone params"); 123 | ht.Add("DRONE_SAVED_TITLE", "Saved drone params"); 124 | ht.Add("DRONE_SAVE_CURRENT", "Save Current Drone"); 125 | ht.Add("DRONE_SAVE_CURRENT_DESC", "Save the current drone parameters."); 126 | ht.Add("DRONE_SPAWN", "Spawn Drone"); 127 | ht.Add("DRONE_SPAWN_DESC", "Spawn this saved drone."); 128 | ht.Add("DRONE_RENAME", "Rename Drone"); 129 | ht.Add("DRONE_RENAME_DESC", "Rename your saved drone."); 130 | ht.Add("DRONE_DELETE", "~r~Delete Drone"); 131 | ht.Add("DRONE_DELETE_DESC", "~r~This will delete your saved drone. Warning: this can NOT be undone!"); 132 | ht.Add("DRONE_MODE", "Mode"); 133 | ht.Add("DRONE_MODE_DESC", "Select drone flight mode.\n" + 134 | "~r~Race drone~s~ - regular, gravity based drone cam\n" + 135 | "~g~Zero-G drone~s~ - gravity set to 0, added deceleration\n" + 136 | "~b~Spectator drone~s~ - easy to operate for spectating\n" + 137 | "~y~Homing drone~s~ - acquire target and keep it in center"); 138 | return ht; 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /EnhancedCamera/MainMenu.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using CitizenFX.Core; 4 | using static CitizenFX.Core.Native.API; 5 | using MenuAPI; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Collections; 9 | using CustomCamera.langs; 10 | 11 | namespace CustomCamera 12 | { 13 | public class MainMenu : BaseScript { 14 | 15 | #region variables 16 | 17 | // Private variables 18 | private Menu Menu; 19 | private static MenuCheckboxItem leadCam; 20 | private static MenuCheckboxItem chaseCam; 21 | private static MenuCheckboxItem droneCam; 22 | private static Control MenuToggleControl; 23 | private static bool chaseCameraConfigEnabled; 24 | private static bool droneCameraConfigEnabled; 25 | 26 | // Public variables 27 | public static CustomCam CustomCamMenu { get; private set; } 28 | public static DroneCam DroneCamMenu { get; private set; } 29 | public static Camera driftCamera = null; 30 | public static Camera chaseCamera = null; 31 | public static Camera droneCamera = null; 32 | public static float userTilt = 0.0f; 33 | public static float userYaw = 0.0f; 34 | public static bool userLookBehind = false; 35 | public static Hashtable langData; 36 | 37 | #endregion 38 | 39 | /// 40 | /// Constructor. 41 | /// 42 | public MainMenu() { 43 | 44 | if (langData == null) { 45 | // Load language data 46 | int langId = GetCurrentLanguage(); 47 | 48 | switch (langId) { 49 | case 9: 50 | langData = new zh_TW().data(); 51 | break; 52 | case 12: 53 | langData = new zh_CN().data(); 54 | break; 55 | default: 56 | langData = new en_US().data(); 57 | break; 58 | } 59 | } 60 | 61 | // Disable menu opening via gamepad (interferes with vMenu) 62 | MenuController.EnableMenuToggleKeyOnController = false; 63 | // Setup menu open/close key 64 | SetConfigParameters(); 65 | // Setup main menu and submenus 66 | CreateSubmenus(); 67 | // Register console command 68 | RegisterCommand("enhancedCam", new Action((source) => { 69 | if (MenuController.IsAnyMenuOpen()) { 70 | MenuController.CloseAllMenus(); 71 | } else { 72 | MenuController.MainMenu.OpenMenu(); 73 | } 74 | }), false); 75 | // Right align menu 76 | try { 77 | MenuController.MenuAlignment = MenuController.MenuAlignmentOption.Right; 78 | }catch(Exception e) { 79 | Debug.WriteLine("[EnhancedCamera] Exception: Cannot align menu to the right."); 80 | } 81 | // Initiate tick 82 | Tick += OnTick; 83 | Tick += GeneralUpdate; 84 | Tick += SlowUpdate; 85 | } 86 | 87 | /// 88 | /// Main OnTick task runs every game tick and handles all the menu stuff. 89 | /// 90 | /// 91 | private async Task OnTick() { 92 | Game.DisableControlThisFrame(0, MenuToggleControl); 93 | } 94 | 95 | #region Setup main menu and submenus 96 | 97 | /// 98 | /// Add the menu to the menu pool and set it up correctly. 99 | /// Also add and bind the menu buttons. 100 | /// 101 | /// 102 | /// 103 | private void AddMenu(Menu parentMenu, Menu submenu, MenuItem menuButton) { 104 | parentMenu.AddMenuItem(menuButton); 105 | MenuController.AddSubmenu(parentMenu, submenu); 106 | MenuController.BindMenuItem(parentMenu, submenu, menuButton); 107 | submenu.RefreshIndex(); 108 | } 109 | 110 | /// 111 | /// Creates all the submenus of main menu 112 | /// 113 | /// 114 | private void CreateSubmenus() { 115 | // Create the menu. 116 | Menu = new Menu(_t("MAIN_MENU_TITLE"), _t("MAIN_MENU_DESC")); 117 | MenuController.AddMenu(Menu); 118 | 119 | #region checkbox items 120 | 121 | // Enabling angular drift cam 122 | leadCam = new MenuCheckboxItem(_t("MAIN_MENU_ENABLE_LEAD"), _t("MAIN_MENU_ENABLE_LEAD_DESC"), false); 123 | // Enabling chase cam 124 | chaseCam = new MenuCheckboxItem(_t("MAIN_MENU_ENABLE_CHASE"), _t("MAIN_MENU_ENABLE_CHASE"), false); 125 | // Enabling chase cam 126 | droneCam = new MenuCheckboxItem(_t("MAIN_MENU_ENABLE_DRONE"), _t("MAIN_MENU_ENABLE_DRONE"), false); 127 | 128 | #endregion 129 | 130 | #region adding menu items 131 | // Checkboxes 132 | Menu.AddMenuItem(leadCam); 133 | if(chaseCameraConfigEnabled) 134 | Menu.AddMenuItem(chaseCam); 135 | if(droneCameraConfigEnabled) 136 | Menu.AddMenuItem(droneCam); 137 | 138 | // Custom cam parameters menu 139 | CustomCamMenu = new CustomCam(); 140 | Menu customCamMenu = CustomCamMenu.GetMenu(); 141 | MenuItem buttonCustom = new MenuItem(_t("MAIN_MENU_LEAD_CHASE_CONF"), _t("MAIN_MENU_LEAD_CHASE_CONF_DESC")) 142 | { 143 | Label = "→→→" 144 | }; 145 | AddMenu(Menu, customCamMenu, buttonCustom); 146 | 147 | // Drone cam parameters menu 148 | DroneCamMenu = new DroneCam(); 149 | Menu droneCamMenu = DroneCamMenu.GetMenu(); 150 | MenuItem buttonDrone = new MenuItem(_t("MAIN_MENU_DRONE_CONF"), _t("MAIN_MENU_DRONE_CONF_DESC")) 151 | { 152 | Label = "→→→" 153 | }; 154 | 155 | if (droneCameraConfigEnabled) 156 | AddMenu(Menu, droneCamMenu, buttonDrone); 157 | 158 | // Credits 159 | MenuItem credits = new MenuItem(_t("MAIN_MENU_CREDITS"), _t("MAIN_MENU_CREDITS_DESC")) {}; 160 | Menu.AddMenuItem(credits); 161 | 162 | #endregion 163 | 164 | #region handling menu changes 165 | 166 | // Handle checkbox changes 167 | Menu.OnCheckboxChange += (_menu, _item, _index, _checked) => { 168 | if (_item == leadCam) 169 | { 170 | CustomCam.LeadCam = _checked; 171 | chaseCam.Checked = false; 172 | droneCam.Checked = false; 173 | CustomCam.ChaseCam = false; 174 | DroneCam.DroneCamVar = false; 175 | 176 | if (!_checked){ ResetCameras(); } 177 | } 178 | if (_item == chaseCam) 179 | { 180 | CustomCam.ChaseCam = _checked; 181 | leadCam.Checked = false; 182 | droneCam.Checked = false; 183 | CustomCam.LeadCam = false; 184 | DroneCam.DroneCamVar = false; 185 | 186 | if (!_checked){ 187 | ResetCameras(); 188 | }else{ 189 | CustomCam.target = CustomCam.GetClosestVehicle(2000, CustomCam.maxAngle); 190 | } 191 | } 192 | if (_item == droneCam) 193 | { 194 | DroneCam.DroneCamVar = _checked; 195 | chaseCam.Checked = false; 196 | leadCam.Checked = false; 197 | CustomCam.ChaseCam = false; 198 | CustomCam.LeadCam = false; 199 | 200 | if (!_checked){ ResetCameras(); } 201 | 202 | } 203 | }; 204 | #endregion 205 | } 206 | 207 | #endregion 208 | 209 | #region math functions 210 | 211 | public class CamMath 212 | { 213 | public const float DegToRad = (float)Math.PI / 180.0f; 214 | 215 | /// 216 | /// Lerps two float values by a step 217 | /// 218 | /// lerped float value in between two supplied 219 | public static float Lerp(float current, float target, float by) 220 | { 221 | return current * (1 - by) + target * by; 222 | } 223 | 224 | /// 225 | /// Calculates angle between two vectors 226 | /// 227 | /// Angle between vectors in degrees 228 | public static float AngleBetween(Vector3 a, Vector3 b) 229 | { 230 | double sinA = a.X * b.Y - b.X * a.Y; 231 | double cosA = a.X * b.X + a.Y * b.Y; 232 | return (float)Math.Atan2(sinA, cosA) / DegToRad; 233 | } 234 | 235 | public static Vector3 RotateRadians(Vector3 v, float degree) 236 | { 237 | float ca = Cos(degree); 238 | float sa = Sin(degree); 239 | return new Vector3(ca * v.X - sa * v.Y, sa * v.X + ca * v.Y, v.Z); 240 | } 241 | 242 | public static Vector3 RotateAroundAxis(Vector3 v, Vector3 axis, float angle) 243 | { 244 | return Vector3.TransformCoordinate(v, Matrix.RotationAxis(Vector3.Normalize(axis), angle)); 245 | } 246 | 247 | public static float Fmod(float a, float b) 248 | { 249 | return (a - b * Floor(a / b)); 250 | } 251 | 252 | public static Vector3 QuaternionToEuler(Quaternion q) 253 | { 254 | double r11 = (-2 * (q.X * q.Y - q.W * q.Z)); 255 | double r12 = (q.W * q.W - q.X * q.X + q.Y * q.Y - q.Z * q.Z); 256 | double r21 = (2 * (q.Y * q.Z + q.W * q.X)); 257 | double r31 = (-2 * (q.X * q.Z - q.W * q.Y)); 258 | double r32 = (q.W * q.W - q.X * q.X - q.Y * q.Y + q.Z * q.Z); 259 | 260 | float ax = (float)Math.Asin(r21); 261 | float ay = (float)Math.Atan2(r31, r32); 262 | float az = (float)Math.Atan2(r11, r12); 263 | 264 | return new Vector3(ax / DegToRad, ay / DegToRad, az / DegToRad); 265 | } 266 | } 267 | 268 | #endregion 269 | 270 | #region camera switching 271 | 272 | public static void SwitchCameraToDrift() 273 | { 274 | SwitchToGameplayCam(); 275 | CustomCam.LeadCam = true; 276 | leadCam.Checked = true; 277 | } 278 | 279 | public static void SwitchCameraToChase() 280 | { 281 | SwitchToGameplayCam(); 282 | CustomCam.ChaseCam = true; 283 | chaseCam.Checked = true; 284 | } 285 | 286 | public static void SwitchToGameplayCam() 287 | { 288 | CustomCam.LeadCam = false; 289 | CustomCam.ChaseCam = false; 290 | DroneCam.DroneCamVar = false; 291 | ResetCameras(); 292 | leadCam.Checked = false; 293 | chaseCam.Checked = false; 294 | droneCam.Checked = false; 295 | } 296 | 297 | #endregion 298 | 299 | #region camera operations 300 | 301 | /// 302 | /// Creates a base camera for lead and chase cam that is not 303 | /// attached to any entity 304 | /// 305 | /// 306 | public static Camera CreateNonAttachedCamera() 307 | { 308 | // Create new camera as a copy of GameplayCamera 309 | Camera newCam = World.CreateCamera(GameplayCamera.Position, GameplayCamera.Rotation, CustomCam.fov); 310 | newCam.IsActive = true; 311 | RenderScriptCams(true, true, 500, true, true); 312 | return newCam; 313 | } 314 | 315 | /// 316 | /// Used to reset lead and chase camera 317 | /// 318 | /// 319 | public static void ResetCameras() 320 | { 321 | RenderScriptCams(false, true, 500, true, true); 322 | driftCamera = null; 323 | chaseCamera = null; 324 | droneCamera = null; 325 | World.DestroyAllCameras(); 326 | SetFocusArea(GameplayCamera.Position.X, GameplayCamera.Position.Y, GameplayCamera.Position.Z, 0, 0, 0); 327 | EnableGameplayCam(true); 328 | UnlockMinimapAngle(); 329 | ClearFocus(); 330 | Game.Player.CanControlCharacter = true; 331 | } 332 | 333 | private const float USER_YAW_RETURN_INTERPOLATION = 0.015f; 334 | private static float yawReturnTimer = 0f; 335 | 336 | /// 337 | /// Additional Update function, currently takes care 338 | /// of user's analog stick up and down movement to 339 | /// control the camera tilt 340 | /// 341 | /// 342 | private async Task GeneralUpdate() 343 | { 344 | if (Menu != null) 345 | { 346 | if (CustomCam.LeadCam || CustomCam.ChaseCam) 347 | { 348 | // User controls the tilt offset 349 | float tiltControl = ((float)(GetControlValue(1, 2) / 256f) - 0.5f); 350 | float yawControl = ((float)(GetControlValue(1, 1) / 256f) - 0.5f); 351 | userLookBehind = IsControlPressed(1, 26); 352 | 353 | if ((Math.Abs(tiltControl) > 0.01f) || (Math.Abs(yawControl) > 0.01f)) 354 | { 355 | //Account for difference in gamepad and mouse acceleration 356 | if (IsInputDisabled(1)) 357 | { 358 | userTilt -= tiltControl * 12f; 359 | userYaw -= yawControl * 32; 360 | } 361 | else 362 | { 363 | userTilt -= tiltControl; 364 | userYaw -= yawControl * 4f; 365 | } 366 | userTilt = (Math.Abs(userTilt) > 80f) ? (Math.Sign(userTilt) * 80f) : (userTilt); 367 | 368 | userYaw = (CamMath.Fmod((userYaw + 180.0f), 360.0f) - 180.0f); 369 | yawReturnTimer = 1f; // Set the timer before yaw starts to return to 0f 370 | 371 | // Slow return of user yaw to 0f 372 | } 373 | else if ((Math.Abs(yawControl) <= 0.01f) && (Math.Abs(userYaw) > (USER_YAW_RETURN_INTERPOLATION + 0.01f))) 374 | { 375 | // Only return to 0f if user is not moving 376 | int vehicleEntity = GetVehiclePedIsIn(PlayerPedId(), false); 377 | if (yawReturnTimer <= 0f) 378 | { 379 | float speedModifier = (Math.Abs(GetEntityVelocity(vehicleEntity).Length()) < 3f) ? (Math.Abs(GetEntityVelocity(vehicleEntity).Length()) / 3f) : (1f); 380 | userYaw = Math.Sign(userYaw) * CamMath.Lerp(Math.Abs(userYaw), 0f, USER_YAW_RETURN_INTERPOLATION * speedModifier); 381 | } 382 | else 383 | { 384 | yawReturnTimer -= USER_YAW_RETURN_INTERPOLATION; 385 | } 386 | } 387 | } 388 | } 389 | else 390 | { 391 | await Delay(1); 392 | } 393 | } 394 | 395 | private async Task SlowUpdate() 396 | { 397 | // Refocus render distance of the camera (too heavy for normal update) 398 | if (Menu != null) 399 | { 400 | if (DroneCam.DroneCamVar) 401 | { 402 | if (droneCamera != null) 403 | { 404 | SetFocusArea(droneCamera.Position.X, droneCamera.Position.Y, droneCamera.Position.Z, 0, 0, 0); 405 | await Delay(250); 406 | } 407 | } 408 | } 409 | else 410 | { 411 | await Delay(1); 412 | } 413 | } 414 | 415 | #endregion 416 | 417 | #region other functions 418 | 419 | public static void Notify(string message) 420 | { 421 | SetNotificationTextEntry("STRING"); 422 | AddTextComponentString3(message); 423 | AddTextComponentSubstringPlayerName("Enhanced Camera"); 424 | DrawNotification(false, false); 425 | } 426 | 427 | private static Dictionary LoadConfig(string filename = "config.ini") { 428 | string stringEntries = null; 429 | stringEntries = LoadResourceFile("enhancedcamera", filename); 430 | Dictionary entries = new Dictionary(); 431 | 432 | var splitted = stringEntries 433 | .Split('\n') 434 | .Where((line) => !line.Trim().StartsWith("#")) 435 | .Select((line) => line.Trim().Split('=')) 436 | .Where((line) => line.Length == 2); 437 | 438 | foreach (var tuple in splitted) { 439 | entries.Add(tuple[0], tuple[1]); 440 | } 441 | return entries; 442 | } 443 | 444 | private static void SetConfigParameters() { 445 | Dictionary config = LoadConfig(); 446 | 447 | // Set menu key 448 | config.TryGetValue("toggleMenu", out string value); 449 | if (int.TryParse(value, out int result)) { 450 | MenuToggleControl = (Control)result; 451 | } else { 452 | MenuToggleControl = (Control)344; 453 | } 454 | MenuController.MenuToggleKey = MenuToggleControl; 455 | 456 | // Set chase and drone camera bools 457 | config.TryGetValue("chaseCameraEnabled", out string chaseCamEnabledStr); 458 | if (int.TryParse(chaseCamEnabledStr, out int chaseCamEnabled)) { 459 | chaseCameraConfigEnabled = (chaseCamEnabled==1)?(true):(false); 460 | } else { 461 | chaseCameraConfigEnabled = true; 462 | } 463 | 464 | config.TryGetValue("droneCameraEnabled", out string droneCameraEnabledStr); 465 | if (int.TryParse(droneCameraEnabledStr, out int droneCameraEnabled)) { 466 | droneCameraConfigEnabled = (droneCameraEnabled == 1) ? (true) : (false); 467 | } else { 468 | droneCameraConfigEnabled = true; 469 | } 470 | 471 | } 472 | 473 | public static async Task GetUserInput(string windowTitle, string defaultText, int maxInputLength) 474 | { 475 | // Create the window title string. 476 | var spacer = "\t"; 477 | AddTextEntry($"{GetCurrentResourceName().ToUpper()}_WINDOW_TITLE", $"{windowTitle ?? "Enter"}:{spacer}(MAX {maxInputLength.ToString()} Characters)"); 478 | 479 | // Display the input box. 480 | DisplayOnscreenKeyboard(1, $"{GetCurrentResourceName().ToUpper()}_WINDOW_TITLE", "", defaultText ?? "", "", "", "", maxInputLength); 481 | await Delay(0); 482 | 483 | // Wait for a result. 484 | while (true){ 485 | int keyboardStatus = UpdateOnscreenKeyboard(); 486 | switch (keyboardStatus){ 487 | case 3: // not displaying input field anymore somehow 488 | case 2: // cancelled 489 | return null; 490 | case 1: // finished editing 491 | return GetOnscreenKeyboardResult(); 492 | default: 493 | await Delay(0); 494 | break; 495 | } 496 | } 497 | } 498 | 499 | private static string _t(string key) { 500 | return Language.get(key); 501 | } 502 | 503 | #endregion 504 | } 505 | } 506 | -------------------------------------------------------------------------------- /EnhancedCamera/menus/DroneCam.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using CitizenFX.Core; 5 | using static CitizenFX.Core.Native.API; 6 | using MenuAPI; 7 | using Newtonsoft.Json; 8 | 9 | namespace CustomCamera 10 | { 11 | public class DroneCam : BaseScript 12 | { 13 | #region variables 14 | 15 | // Public variables 16 | public static bool DroneCamVar = false; 17 | 18 | // Private variables 19 | private Menu menu; 20 | private Menu savedDronesMenu; 21 | private Menu selectedDroneMenu = new Menu(_t("DRONE_MANAGE_TITLE"), _t("DRONE_MANAGE_DESC")); 22 | private MenuListItem modeList; 23 | private Dictionary> sdMenuItems = new Dictionary>(); 24 | private static KeyValuePair currentlySelectedDrone = new KeyValuePair(); 25 | private static int droneMode = 0; 26 | private static bool invertedPitch = false; 27 | private static bool invertedRoll = false; 28 | private static Vehicle homingTarget = null; 29 | 30 | #endregion 31 | 32 | #region GUI updating 33 | 34 | // GUI parameters 35 | MenuListItem gravityMultList; 36 | MenuListItem timestepMultList; 37 | MenuListItem dragMultList; 38 | MenuListItem accelerationMultList; 39 | MenuListItem rotationMultXList; 40 | MenuListItem rotationMultYList; 41 | MenuListItem rotationMultZList; 42 | MenuListItem tiltAngleList; 43 | MenuListItem fovList; 44 | MenuListItem maxVelList; 45 | 46 | MenuCheckboxItem invertPitch; 47 | MenuCheckboxItem invertRoll; 48 | 49 | // Update params 50 | private void UpdateParams() 51 | { 52 | // Reset camera to update params 53 | MainMenu.ResetCameras(); 54 | if (DroneCamVar) 55 | { 56 | CreateDroneCamera(); 57 | } 58 | // Update GUI params 59 | gravityMultList.ListIndex = (int)((gravityMult - 0.5f) / 0.05f); 60 | timestepMultList.ListIndex = (int)((timestepMult - 0.5f) / 0.05f); 61 | dragMultList.ListIndex = (int)((dragMult) / 0.05f); 62 | accelerationMultList.ListIndex = (int)((accelerationMult - 0.5f) / 0.05f); 63 | rotationMultXList.ListIndex = (int)((rotationMult.X - 0.5f) / 0.05f); 64 | rotationMultYList.ListIndex = (int)((rotationMult.Y - 0.5f) / 0.05f); 65 | rotationMultZList.ListIndex = (int)((rotationMult.Z - 0.5f) / 0.05f); 66 | maxVelList.ListIndex = (int)(maxVel - 10f); 67 | tiltAngleList.ListIndex = (int)((tiltAngle) / 5.0f); 68 | fovList.ListIndex = (int)((droneFov - 30.0f) / 5.0f); 69 | 70 | menu.RefreshIndex(); 71 | } 72 | 73 | #endregion 74 | 75 | // Constructor 76 | public DroneCam() 77 | { 78 | Tick += RunDroneCam; 79 | Tick += AntiAfk; 80 | } 81 | 82 | private void CreateMenu() 83 | { 84 | menu = new Menu(_t("DRONE_TITLE"), _t("DRONE_DESC")); 85 | 86 | #region main parameters 87 | 88 | // Drone modes 89 | List modeListData = new List() { _t("DRONE_RACE"), _t("DRONE_ZERO_G"), _t("DRONE_SPECTATOR"), _t("DRONE_HOMING") }; 90 | modeList = new MenuListItem(_t("DRONE_MODE"), modeListData, 0, _t("DRONE_MODE_DESC")); 91 | 92 | // Invert input 93 | invertPitch = new MenuCheckboxItem(_t("DRONE_INVERT_PITCH"), _t("DRONE_INVERT_PITCH_DESC"), false); 94 | invertRoll = new MenuCheckboxItem(_t("DRONE_INVERT_ROLL"), _t("DRONE_INVERT_ROLL_DESC"), false); 95 | 96 | // Gravity multiplier 97 | List gravityMultValues = new List(); 98 | for (float i = 0.5f; i <= 4.0f; i += 0.05f) 99 | { 100 | gravityMultValues.Add(i.ToString("0.00")); 101 | } 102 | gravityMultList = new MenuListItem(_t("DRONE_GRAVITY"), gravityMultValues, 10, _t("DRONE_GRAVITY_DESC")) 103 | { 104 | ShowColorPanel = false 105 | }; 106 | 107 | // Timestep multiplier 108 | List timestepValues = new List(); 109 | for (float i = 0.5f; i <= 4.0f; i += 0.05f) 110 | { 111 | timestepValues.Add(i.ToString("0.00")); 112 | } 113 | timestepMultList = new MenuListItem(_t("DRONE_TIMESTEP"), timestepValues, 10, _t("DRONE_TIMESTEP_DESC")) 114 | { 115 | ShowColorPanel = false 116 | }; 117 | 118 | // Drag multiplier 119 | List dragMultValues = new List(); 120 | for (float i = 0.0f; i <= 4.0f; i += 0.05f) 121 | { 122 | dragMultValues.Add(i.ToString("0.00")); 123 | } 124 | dragMultList = new MenuListItem(_t("DRONE_DRAG"), dragMultValues, 20, _t("DRONE_DRAG_DESC")) 125 | { 126 | ShowColorPanel = false 127 | }; 128 | 129 | // Acceleration multiplier 130 | List accelerationMultValues = new List(); 131 | for (float i = 0.5f; i <= 4.0f; i += 0.05f) 132 | { 133 | accelerationMultValues.Add(i.ToString("0.00")); 134 | } 135 | accelerationMultList = new MenuListItem(_t("DRONE_ACCELE"), accelerationMultValues, 10, _t("DRONE_ACCELE_DESC")) 136 | { 137 | ShowColorPanel = false 138 | }; 139 | 140 | // Rotation multipliers 141 | List rotationMultXValues = new List(); 142 | for (float i = 0.5f; i <= 4.0f; i += 0.05f) 143 | { 144 | rotationMultXValues.Add(i.ToString("0.00")); 145 | } 146 | rotationMultXList = new MenuListItem(_t("DRONE_PITCH"), rotationMultXValues, 10, _t("DRONE_PITCH_DESC")) 147 | { 148 | ShowColorPanel = false 149 | }; 150 | List rotationMultYValues = new List(); 151 | for (float i = 0.5f; i <= 4.0f; i += 0.05f) 152 | { 153 | rotationMultYValues.Add(i.ToString("0.00")); 154 | } 155 | rotationMultYList = new MenuListItem(_t("DRONE_ROLL"), rotationMultYValues, 10, _t("DRONE_ROLL_DESC")) 156 | { 157 | ShowColorPanel = false 158 | }; 159 | List rotationMultZValues = new List(); 160 | for (float i = 0.5f; i <= 4.0f; i += 0.05f) 161 | { 162 | rotationMultZValues.Add(i.ToString("0.00")); 163 | } 164 | rotationMultZList = new MenuListItem(_t("DRONE_YAW"), rotationMultZValues, 10, _t("DRONE_YAW_DESC")) 165 | { 166 | ShowColorPanel = false 167 | }; 168 | // Tilt angle 169 | List tiltAngleValues = new List(); 170 | for (float i = 0.0f; i <= 80.0f; i += 5f) 171 | { 172 | tiltAngleValues.Add(i.ToString("0.0")); 173 | } 174 | tiltAngleList = new MenuListItem(_t("DRONE_TILT"), tiltAngleValues, 9, _t("DRONE_TILT_DESC")) 175 | { 176 | ShowColorPanel = false 177 | }; 178 | // FOV 179 | List fovValues = new List(); 180 | for (float i = 30.0f; i <= 120.0f; i += 5f) 181 | { 182 | fovValues.Add(i.ToString("0.0")); 183 | } 184 | fovList = new MenuListItem(_t("DRONE_FOV"), fovValues, 10, _t("DRONE_FOV_DESC")) 185 | { 186 | ShowColorPanel = false 187 | }; 188 | // Max velocity 189 | List maxVelValues = new List(); 190 | for (float i = 10.0f; i <= 50.0f; i += 1f) 191 | { 192 | maxVelValues.Add(i.ToString("0.0")); 193 | } 194 | maxVelList = new MenuListItem(_t("DRONE_MAX_VELOCITY"), maxVelValues, 20, _t("DRONE_MAX_VELOCITY_DESC")) 195 | { 196 | ShowColorPanel = false 197 | }; 198 | 199 | #endregion 200 | 201 | #region adding menu items 202 | 203 | menu.AddMenuItem(modeList); 204 | menu.AddMenuItem(invertPitch); 205 | menu.AddMenuItem(invertRoll); 206 | menu.AddMenuItem(gravityMultList); 207 | menu.AddMenuItem(timestepMultList); 208 | menu.AddMenuItem(dragMultList); 209 | menu.AddMenuItem(accelerationMultList); 210 | menu.AddMenuItem(maxVelList); 211 | menu.AddMenuItem(rotationMultXList); 212 | menu.AddMenuItem(rotationMultYList); 213 | menu.AddMenuItem(rotationMultZList); 214 | menu.AddMenuItem(tiltAngleList); 215 | menu.AddMenuItem(fovList); 216 | 217 | #endregion 218 | 219 | #region managing save/load camera stuff 220 | 221 | // Saving/Loading cameras 222 | MenuItem savedDronesButton = new MenuItem(_t("DRONE_SAVED"), _t("DRONE_SAVED_DESC")); 223 | savedDronesMenu = new Menu(_t("DRONE_SAVED_TITLE")); 224 | MenuController.AddSubmenu(menu, savedDronesMenu); 225 | menu.AddMenuItem(savedDronesButton); 226 | savedDronesButton.Label = "→→→"; 227 | MenuController.BindMenuItem(menu, savedDronesMenu, savedDronesButton); 228 | 229 | MenuItem saveDrone = new MenuItem(_t("DRONE_SAVE_CURRENT"), _t("DRONE_SAVE_CURRENT_DESC")); 230 | savedDronesMenu.AddMenuItem(saveDrone); 231 | savedDronesMenu.OnMenuOpen += (sender) => { 232 | savedDronesMenu.ClearMenuItems(); 233 | savedDronesMenu.AddMenuItem(saveDrone); 234 | LoadDroneCameras(); 235 | }; 236 | 237 | savedDronesMenu.OnItemSelect += (sender, item, index) => { 238 | if (item == saveDrone) 239 | { 240 | SaveCamera(); 241 | savedDronesMenu.GoBack(); 242 | } 243 | else 244 | { 245 | UpdateSelectedCameraMenu(item, sender); 246 | } 247 | }; 248 | 249 | MenuController.AddMenu(selectedDroneMenu); 250 | MenuItem spawnCamera = new MenuItem(_t("DRONE_SPAWN"), _t("DRONE_SPAWN_DESC")); 251 | MenuItem renameCamera = new MenuItem(_t("DRONE_RENAME"), _t("DRONE_RENAME_DESC")); 252 | MenuItem deleteCamera = new MenuItem(_t("DRONE_DELETE"), _t("DRONE_DELETE_DESC")); 253 | selectedDroneMenu.AddMenuItem(spawnCamera); 254 | selectedDroneMenu.AddMenuItem(renameCamera); 255 | selectedDroneMenu.AddMenuItem(deleteCamera); 256 | 257 | selectedDroneMenu.OnMenuClose += (sender) => { 258 | selectedDroneMenu.RefreshIndex(); 259 | }; 260 | 261 | selectedDroneMenu.OnItemSelect += async (sender, item, index) => { 262 | if (item == spawnCamera) 263 | { 264 | MainMenu.ResetCameras(); 265 | SpawnSavedCamera(); 266 | UpdateParams(); 267 | selectedDroneMenu.GoBack(); 268 | savedDronesMenu.RefreshIndex(); 269 | 270 | } 271 | else if (item == deleteCamera) 272 | { 273 | item.Label = ""; 274 | DeleteResourceKvp(currentlySelectedDrone.Key); 275 | selectedDroneMenu.GoBack(); 276 | savedDronesMenu.RefreshIndex(); 277 | MainMenu.Notify("~g~~h~Info~h~~s~: Your saved drone has been deleted."); 278 | } 279 | else if (item == renameCamera) 280 | { 281 | string newName = await MainMenu.GetUserInput(windowTitle: "Enter a new name for this drone.", defaultText: null, maxInputLength: 30); 282 | if (string.IsNullOrEmpty(newName)) 283 | { 284 | MainMenu.Notify("~r~~h~Error~h~~s~: Invalid input"); 285 | } 286 | else 287 | { 288 | if (SaveCameraInfo("xdm_" + newName, currentlySelectedDrone.Value, false)) 289 | { 290 | DeleteResourceKvp(currentlySelectedDrone.Key); 291 | while (!selectedDroneMenu.Visible) 292 | { 293 | await BaseScript.Delay(0); 294 | } 295 | MainMenu.Notify("~g~~h~Info~h~~s~: Your drone has successfully been renamed."); 296 | selectedDroneMenu.GoBack(); 297 | currentlySelectedDrone = new KeyValuePair(); 298 | } 299 | else 300 | { 301 | MainMenu.Notify("~r~~h~Error~h~~s~: This name is already in use or something unknown failed. Contact the server owner if you believe something is wrong."); 302 | } 303 | } 304 | } 305 | }; 306 | 307 | #endregion 308 | 309 | #region handling menu changes 310 | 311 | // Handle checkbox changes 312 | menu.OnCheckboxChange += (_menu, _item, _index, _checked) => { 313 | if (_item == invertPitch) 314 | { 315 | invertedPitch = _checked; 316 | } 317 | else if (_item == invertRoll) 318 | { 319 | invertedRoll = _checked; 320 | } 321 | }; 322 | 323 | // Handle sliders 324 | menu.OnListIndexChange += (_menu, _listItem, _oldIndex, _newIndex, _itemIndex) => { 325 | if (_listItem == modeList) 326 | { 327 | droneMode = _newIndex; 328 | if (droneMode == 3) 329 | { 330 | homingTarget = GetClosestVehicleToDrone(2000); 331 | } 332 | } 333 | 334 | if (_listItem == gravityMultList) 335 | { 336 | gravityMult = _newIndex * 0.05f + 0.5f; 337 | } 338 | if (_listItem == timestepMultList) 339 | { 340 | timestepMult = _newIndex * 0.05f + 0.5f; 341 | } 342 | if (_listItem == dragMultList) 343 | { 344 | dragMult = _newIndex * 0.05f; 345 | } 346 | if (_listItem == accelerationMultList) 347 | { 348 | accelerationMult = _newIndex * 0.05f + 0.5f; 349 | } 350 | 351 | if (_listItem == rotationMultXList) 352 | { 353 | rotationMult.X = _newIndex * 0.05f + 0.5f; 354 | } 355 | if (_listItem == rotationMultYList) 356 | { 357 | rotationMult.Y = _newIndex * 0.05f + 0.5f; 358 | } 359 | if (_listItem == rotationMultZList) 360 | { 361 | rotationMult.Z = _newIndex * 0.05f + 0.5f; 362 | } 363 | 364 | if (_listItem == maxVelList) 365 | { 366 | maxVel = _newIndex * 1f + 10f; 367 | } 368 | 369 | if (_listItem == tiltAngleList) 370 | { 371 | tiltAngle = _newIndex * 5.0f; 372 | } 373 | if (_listItem == fovList) 374 | { 375 | droneFov = _newIndex * 5.0f + 30f; 376 | if (MainMenu.droneCamera != null) 377 | { 378 | SetCamFov(MainMenu.droneCamera.Handle, droneFov); 379 | } 380 | } 381 | }; 382 | 383 | #endregion 384 | } 385 | 386 | /// 387 | /// Creates the menu if it doesn't exist, and then returns it. 388 | /// 389 | /// The Menu 390 | public Menu GetMenu() 391 | { 392 | if (menu == null) 393 | { 394 | CreateMenu(); 395 | } 396 | return menu; 397 | } 398 | 399 | #region params 400 | 401 | private DroneInfo drone; 402 | 403 | // Parameters for user to tune 404 | private static float gravityMult = 1.0f; 405 | private static float timestepMult = 1.0f; 406 | private static float dragMult = 1.0f; 407 | private static Vector3 rotationMult = new Vector3(1f, 1f, 1f); 408 | private static float accelerationMult = 1f; 409 | private static float tiltAngle = 45.0f; 410 | private static float droneFov = 80.0f; 411 | private static float maxVel = 30.0f; 412 | 413 | // Const drone parameters 414 | private const float GRAVITY_CONST = 9.8f; // Gravity force constant 415 | private const float TIMESTEP_DELIMITER = 90.15f; // Less - gravity is stronger 416 | private const float DRONE_DRAG = 0.0020f; // Air resistance 417 | private const float DRONE_AGILITY_ROT = 55000f; // How quick is rotational response of the drone 418 | private const float DRONE_AGILITY_VEL = 210f; // How quick is velocity and acceleration response 419 | private const float DRONE_MAX_VELOCITY = 0.01f; // Max velocity of the drone 420 | 421 | #endregion 422 | 423 | #region main functions 424 | 425 | /// 426 | /// Changes main render camera behaviour, creates a free camera controlled 427 | /// like a drone. 428 | /// 429 | /// 430 | private async Task RunDroneCam() 431 | { 432 | if (DroneCamVar) 433 | { 434 | if (MainMenu.droneCamera != null) 435 | { 436 | // Get user input 437 | UpdateDroneControls(); 438 | 439 | // Update camera properties 440 | if (droneMode == 2) 441 | { // Spectate mode 442 | UpdateDronePositionSpectate(); 443 | UpdateDroneRotationSpectate(); 444 | } 445 | else if (droneMode == 3) 446 | { // Homing mode 447 | UpdateDronePositionSpectate(); 448 | UpdateDroneRotationHoming(); 449 | } 450 | else 451 | { 452 | UpdateDronePosition(); 453 | UpdateDroneRotation(); 454 | } 455 | } 456 | else 457 | { 458 | CreateDroneCamera(); 459 | } 460 | } 461 | else 462 | { 463 | await Delay(0); 464 | } 465 | } 466 | 467 | /// 468 | /// Move player a bit every 250 seconds to avoid AFK kick 469 | /// when using drone. 470 | /// 471 | /// 472 | private async Task AntiAfk() 473 | { 474 | if (menu != null) 475 | { 476 | if (DroneCamVar) 477 | { 478 | SimulatePlayerInputGait(Game.Player.Handle, 1.0f, 100, 0.2f, true, false); 479 | await Delay(250000); 480 | } 481 | } 482 | else 483 | { 484 | await Delay(0); 485 | } 486 | } 487 | 488 | private void CreateDroneCamera() 489 | { 490 | MainMenu.ResetCameras(); 491 | MainMenu.droneCamera = MainMenu.CreateNonAttachedCamera(); 492 | MainMenu.droneCamera.FieldOfView = droneFov; 493 | MainMenu.droneCamera.IsActive = true; 494 | drone = new DroneInfo 495 | { 496 | velocity = Vector3.Zero, 497 | downVelocity = 0f, 498 | rotation = new Quaternion(0f, 0f, 0f, 1f) 499 | }; 500 | Game.Player.CanControlCharacter = false; 501 | } 502 | 503 | // Struct containing all the necessary info for tracking drone 504 | // movement. 505 | private struct DroneInfo 506 | { 507 | // User input 508 | public float acceleration; 509 | public float deceleration; 510 | public float controlPitch; 511 | public float controlYaw; 512 | public float controlRoll; 513 | // Current values 514 | public Vector3 velocity; // Drone's velocity in all directions 515 | public float downVelocity; // Velocity caused by gravity 516 | public Quaternion rotation; // Drone rotation in quaternion 517 | } 518 | 519 | private void DumpDebug() 520 | { 521 | Debug.WriteLine(drone.acceleration.ToString() + 522 | drone.controlPitch.ToString() + 523 | drone.controlYaw.ToString() + 524 | drone.controlRoll.ToString() + 525 | drone.velocity.ToString() + 526 | drone.downVelocity.ToString() + 527 | drone.rotation.X.ToString() + 528 | drone.rotation.Y.ToString() + 529 | drone.rotation.Z.ToString() + 530 | drone.rotation.W.ToString() + 531 | MainMenu.droneCamera.Position.ToString() 532 | ); 533 | } 534 | 535 | // Get user input for drone camera 536 | private void UpdateDroneControls() 537 | { 538 | drone.acceleration = ((GetDisabledControlNormal(0, 71)) / 2f); 539 | drone.deceleration = ((GetDisabledControlNormal(0, 72)) / 2f); 540 | drone.controlPitch = ((GetDisabledControlNormal(1, 2)) / 2f); 541 | drone.controlYaw = -((GetDisabledControlNormal(1, 9)) / 2f); 542 | drone.controlRoll = ((GetDisabledControlNormal(1, 1)) / 2f); 543 | 544 | // Account for mouse controls 545 | if (IsInputDisabled(1)) 546 | { 547 | drone.controlPitch *= 3.5f; 548 | drone.controlYaw *= 0.55f; 549 | drone.controlRoll *= 4.5f; 550 | } 551 | } 552 | 553 | #endregion 554 | 555 | #region race mode 556 | 557 | // Update drone's rotation based on input 558 | private void UpdateDroneRotation() 559 | { 560 | float deltaTime = timestepMult * Timestep() / TIMESTEP_DELIMITER; 561 | 562 | // Calculate delta of rotation based on user input 563 | float deltaPitch = drone.controlPitch * DRONE_AGILITY_ROT * 0.70f * rotationMult.X * deltaTime; 564 | float deltaYaw = drone.controlYaw * DRONE_AGILITY_ROT * 0.6f * rotationMult.Z * deltaTime; 565 | float deltaRoll = drone.controlRoll * DRONE_AGILITY_ROT * 0.75f * rotationMult.Y * deltaTime; 566 | 567 | // Account for inverted axes 568 | deltaPitch *= (invertedPitch) ? (-1f) : (1f); 569 | deltaRoll *= (invertedRoll) ? (-1f) : (1f); 570 | 571 | // Rotate quaternion 572 | drone.rotation *= Quaternion.RotationAxis(Vector3.Up, deltaRoll * MainMenu.CamMath.DegToRad); 573 | drone.rotation *= Quaternion.RotationAxis(Vector3.Right, deltaPitch * MainMenu.CamMath.DegToRad); 574 | drone.rotation *= Quaternion.RotationAxis(Vector3.ForwardLH, deltaYaw * MainMenu.CamMath.DegToRad); 575 | 576 | // Update camera rotation based on values 577 | Vector3 eulerRot = MainMenu.CamMath.QuaternionToEuler(drone.rotation); 578 | SetCamRot(MainMenu.droneCamera.Handle, eulerRot.X, eulerRot.Y, eulerRot.Z, 2); 579 | } 580 | 581 | // Implementation of drone's physics engine 582 | private void UpdateDronePosition() 583 | { 584 | // For dividing velocity into two vectors based on camera tilt 585 | // compared to drone itself 586 | float staticTilt = Tan(tiltAngle); 587 | 588 | // Timeframe used for calculations 589 | float deltaTime = timestepMult * Timestep() / TIMESTEP_DELIMITER; 590 | 591 | // Calculate impact of gravity force 592 | float deltaDownForce = GRAVITY_CONST * gravityMult; // F = m*a = m*g 593 | 594 | // Calculate velocity based on acceleration 595 | // Drone is tilted compared to camera, so there are two vectors 596 | // Forward and up are opposite due to naming conventions mismatch 597 | float deltaVelocityForward = drone.acceleration * DRONE_AGILITY_VEL * accelerationMult * 0.5f * deltaTime; // dV = a*dt 598 | float deltaVelocityUp = drone.acceleration * DRONE_AGILITY_VEL * accelerationMult * (staticTilt / 2f) * deltaTime; // dV = a*dt 599 | // Enable deceleration when in zero-G mode and get rid of gravity force 600 | if (droneMode == 1) 601 | { 602 | deltaVelocityForward -= drone.deceleration * DRONE_AGILITY_VEL * accelerationMult * 0.5f * deltaTime; 603 | deltaVelocityUp += drone.deceleration * DRONE_AGILITY_VEL * accelerationMult * (staticTilt / 2f) * deltaTime; 604 | deltaDownForce = 0f; 605 | } 606 | 607 | // Additional 2x boost on spacebar/R1 608 | float boost = (GetDisabledControlNormal(1, 102) + 1f); 609 | 610 | drone.velocity += MainMenu.droneCamera.ForwardVector * deltaVelocityForward * boost; // V1 = V0 + dV 611 | drone.velocity -= MainMenu.droneCamera.UpVector * deltaVelocityUp * boost; // V1 = V0 + dV 612 | // Account for air resistance 613 | drone.velocity -= drone.velocity * DRONE_DRAG * dragMult; 614 | drone.velocity += Vector3.ForwardLH * deltaDownForce * deltaTime; 615 | 616 | // Clamp velocity to maximum with some smoothing 617 | if (Math.Abs(drone.velocity.Length()) > boost * maxVel * DRONE_MAX_VELOCITY) 618 | { 619 | drone.velocity = Vector3.Lerp(drone.velocity, drone.velocity * boost * maxVel * DRONE_MAX_VELOCITY / drone.velocity.Length(), 0.08f); 620 | } 621 | 622 | // Update camera position based on velocity values 623 | MainMenu.droneCamera.Position -= drone.velocity; 624 | } 625 | 626 | #endregion 627 | 628 | #region spectator mode 629 | 630 | // Special update functions for spectator mode drone 631 | private void UpdateDroneRotationSpectate() 632 | { 633 | float deltaTime = timestepMult * Timestep() / TIMESTEP_DELIMITER; 634 | 635 | // Calculate delta of rotation based on user input 636 | float deltaPitch = -drone.controlPitch * DRONE_AGILITY_ROT * 0.70f * rotationMult.X * deltaTime; 637 | float deltaYaw = -drone.controlRoll * DRONE_AGILITY_ROT * 0.6f * rotationMult.Z * deltaTime; 638 | 639 | // Account for inverted axes 640 | deltaPitch *= (invertedPitch) ? (-1f) : (1f); 641 | 642 | // Update camera rotation based on values 643 | SetCamRot(MainMenu.droneCamera.Handle, 644 | Math.Abs(MainMenu.droneCamera.Rotation.X + deltaPitch) < 89f ? (MainMenu.droneCamera.Rotation.X + deltaPitch) 645 | : (Math.Sign(MainMenu.droneCamera.Rotation.X) * 88.9f), 646 | 0f, 647 | MainMenu.droneCamera.Rotation.Z + deltaYaw, 648 | 2); 649 | } 650 | 651 | private void UpdateDronePositionSpectate() 652 | { 653 | float deltaTime = timestepMult * Timestep() / TIMESTEP_DELIMITER; 654 | 655 | float deltaForward = -((GetDisabledControlNormal(1, 31)) / 2f) * deltaTime * DRONE_AGILITY_VEL * accelerationMult / 2f; 656 | float deltaSide = ((GetDisabledControlNormal(1, 30)) / 2f) * deltaTime * DRONE_AGILITY_VEL * accelerationMult / 2f; 657 | float deltaUp = ((GetDisabledControlNormal(1, 92)) / 2f) * deltaTime * DRONE_AGILITY_VEL * accelerationMult; 658 | float deltaDown = ((GetDisabledControlNormal(1, 91)) / 2f) * deltaTime * DRONE_AGILITY_VEL * accelerationMult; 659 | 660 | // Additional 2x boost on spacebar/R1 661 | float boost = (GetDisabledControlNormal(1, 102) + 1f); 662 | 663 | Vector3 dir = MainMenu.CamMath.RotateAroundAxis(MainMenu.droneCamera.Direction, MainMenu.droneCamera.RightVector, 90f * MainMenu.CamMath.DegToRad); 664 | 665 | drone.velocity -= Vector3.Normalize(new Vector3(dir.X, 666 | dir.Y, 667 | 0f)) * deltaForward * boost; 668 | drone.velocity += MainMenu.CamMath.RotateAroundAxis( 669 | Vector3.Normalize(new Vector3(dir.X, 670 | dir.Y, 671 | 0f)), 672 | Vector3.ForwardLH, 673 | 90f * MainMenu.CamMath.DegToRad 674 | ) * deltaSide * boost; 675 | 676 | 677 | // Account for air ressistance 678 | drone.velocity -= drone.velocity * DRONE_DRAG * dragMult; 679 | 680 | drone.velocity -= Vector3.ForwardLH * deltaUp; 681 | drone.velocity += Vector3.ForwardLH * deltaDown; 682 | 683 | // Clamp velocity to maximum with some smoothing 684 | if (Math.Abs(drone.velocity.Length()) > maxVel * boost * DRONE_MAX_VELOCITY) 685 | { 686 | drone.velocity = Vector3.Lerp(drone.velocity, drone.velocity * boost * maxVel * DRONE_MAX_VELOCITY / drone.velocity.Length(), 0.08f); 687 | } 688 | 689 | // Update camera position based on velocity values 690 | MainMenu.droneCamera.Position -= drone.velocity; 691 | } 692 | 693 | #endregion 694 | 695 | #region homing mode 696 | 697 | /// 698 | /// Gets closest vehicle to Camera 699 | /// 700 | /// closest vehicle 701 | private Vehicle GetClosestVehicleToDrone(int maxDistance) 702 | { 703 | float smallestDistance = (float)maxDistance; 704 | Vehicle[] vehs = World.GetAllVehicles(); 705 | Vehicle closestVeh = null; 706 | 707 | if (vehs != null) 708 | { 709 | foreach (Vehicle veh in vehs) 710 | { 711 | float distance = Vector3.Distance(GetEntityCoords(veh.Handle, true), MainMenu.droneCamera.Position); 712 | if ((distance <= smallestDistance) && (veh != null)) 713 | { 714 | smallestDistance = distance; 715 | closestVeh = veh; 716 | } 717 | } 718 | } 719 | return closestVeh; 720 | } 721 | 722 | private void UpdateDroneRotationHoming() 723 | { 724 | float deltaTime = timestepMult * Timestep() / TIMESTEP_DELIMITER; 725 | 726 | if (homingTarget != null) 727 | { 728 | Vector3 targetDir = homingTarget.Position - MainMenu.droneCamera.Position; 729 | 730 | MainMenu.droneCamera.Direction = targetDir; 731 | } 732 | } 733 | 734 | #endregion 735 | 736 | /// --- 737 | /// Save/load functions originally made by Vespura (https://www.tomgrobbe.com/) for vMenu. 738 | /// Snippets of the code were slightly modified to suit camera needs and added here. 739 | /// --- 740 | #region save/load 741 | 742 | public struct DroneSaveInfo 743 | { 744 | public float gravityMult_; 745 | public float timestepMult_; 746 | public float dragMult_; 747 | public float accelerationMult_; 748 | public Vector3 rotationMult_; 749 | public float maxVel_; 750 | public float tiltAngle_; 751 | public float droneFov_; 752 | } 753 | 754 | private bool UpdateSelectedCameraMenu(MenuItem selectedItem, Menu parentMenu = null) 755 | { 756 | if (!sdMenuItems.ContainsKey(selectedItem)) 757 | { 758 | MainMenu.Notify("~r~~h~Error~h~~s~: In some very strange way, you've managed to select a button, that does not exist according to this list. So your vehicle could not be loaded. :( Maybe your save files are broken?"); 759 | return false; 760 | } 761 | var camInfo = sdMenuItems[selectedItem]; 762 | currentlySelectedDrone = camInfo; 763 | selectedDroneMenu.MenuSubtitle = $"{camInfo.Key.Substring(4)}"; 764 | MenuController.CloseAllMenus(); 765 | selectedDroneMenu.OpenMenu(); 766 | if (parentMenu != null) 767 | { 768 | MenuController.AddSubmenu(parentMenu, selectedDroneMenu); 769 | } 770 | return true; 771 | } 772 | 773 | private bool SpawnSavedCamera() 774 | { 775 | if (currentlySelectedDrone.Key != null) 776 | { 777 | gravityMult = currentlySelectedDrone.Value.gravityMult_; 778 | timestepMult = currentlySelectedDrone.Value.timestepMult_; 779 | dragMult = currentlySelectedDrone.Value.dragMult_; 780 | accelerationMult = currentlySelectedDrone.Value.accelerationMult_; 781 | rotationMult = currentlySelectedDrone.Value.rotationMult_; 782 | maxVel = currentlySelectedDrone.Value.maxVel_; 783 | tiltAngle = currentlySelectedDrone.Value.tiltAngle_; 784 | droneFov = currentlySelectedDrone.Value.droneFov_; 785 | } 786 | else 787 | { 788 | MainMenu.Notify("~r~~h~Error~h~~s~: It seems that this slot got corrupted in some way, you need to delete it."); 789 | return false; 790 | } 791 | return true; 792 | } 793 | 794 | private bool SaveCameraInfo(string saveName, DroneSaveInfo cameraInfo, bool overrideOldVersion) 795 | { 796 | if (string.IsNullOrEmpty(GetResourceKvpString(saveName)) || overrideOldVersion) 797 | { 798 | if (!string.IsNullOrEmpty(saveName) && saveName.Length > 4) 799 | { 800 | // convert 801 | string json = JsonConvert.SerializeObject(cameraInfo); 802 | 803 | // log 804 | Debug.WriteLine($"Saving!\nName: {saveName}\nDrone Data: {json}\n"); 805 | 806 | // save 807 | SetResourceKvp(saveName, json); 808 | 809 | // confirm 810 | return GetResourceKvpString(saveName) == json; 811 | } 812 | } 813 | // if something isn't right, then the save is aborted and return false ("failed" state). 814 | return false; 815 | } 816 | 817 | public async void SaveCamera(string updateExistingSavedCameraName = null) 818 | { 819 | DroneSaveInfo ci = new DroneSaveInfo() 820 | { 821 | gravityMult_ = gravityMult, 822 | timestepMult_ = timestepMult, 823 | dragMult_ = dragMult, 824 | accelerationMult_ = accelerationMult, 825 | rotationMult_ = rotationMult, 826 | maxVel_ = maxVel, 827 | tiltAngle_ = tiltAngle, 828 | droneFov_ = droneFov 829 | }; 830 | 831 | if (updateExistingSavedCameraName == null) 832 | { 833 | var saveName = await MainMenu.GetUserInput(windowTitle: "Enter a save name", defaultText: null, maxInputLength: 30); 834 | // If the name is not invalid. 835 | if (!string.IsNullOrEmpty(saveName)) 836 | { 837 | // Save everything from the dictionary into the client's kvp storage. 838 | // If the save was successfull: 839 | if (SaveCameraInfo("xdm_" + saveName, ci, false)) 840 | { 841 | MainMenu.Notify($"~g~~h~Info~h~~s~: Drone {saveName} saved."); 842 | LoadDroneCameras(); 843 | } 844 | // If the save was not successfull: 845 | else 846 | { 847 | MainMenu.Notify("~r~~h~Error~h~~s~: Save already exists: (" + saveName + ")"); 848 | } 849 | } 850 | // The user did not enter a valid name to use as a save name for this vehicle. 851 | else 852 | { 853 | MainMenu.Notify("~r~~h~Error~h~~s~: Invalid save name"); 854 | } 855 | } 856 | // We need to update an existing slot. 857 | else 858 | { 859 | SaveCameraInfo("xdm_" + updateExistingSavedCameraName, ci, true); 860 | } 861 | } 862 | 863 | private Dictionary GetSavedCameras() 864 | { 865 | // Create a list to store all saved camera names in. 866 | var savedCameraNames = new List(); 867 | // Start looking for kvps starting with xcm_ 868 | var findHandle = StartFindKvp("xdm_"); 869 | // Keep looking... 870 | while (true) 871 | { 872 | // Get the kvp string key. 873 | var camString = FindKvp(findHandle); 874 | 875 | // If it exists then the key to the list. 876 | if (camString != "" && camString != null && camString != "NULL") 877 | { 878 | savedCameraNames.Add(camString); 879 | } 880 | // Otherwise stop. 881 | else 882 | { 883 | EndFindKvp(findHandle); 884 | break; 885 | } 886 | } 887 | var camerasList = new Dictionary(); 888 | // Loop through all save names (keys) from the list above, convert the string into a dictionary 889 | // and add it to the dictionary above, with the camera save name as the key. 890 | foreach (var saveName in savedCameraNames) 891 | { 892 | camerasList.Add(saveName, JsonConvert.DeserializeObject(GetResourceKvpString(saveName))); 893 | } 894 | // Return the camera dictionary containing all camera save names (keys) linked to the correct camera 895 | return camerasList; 896 | } 897 | 898 | private async void LoadDroneCameras() 899 | { 900 | var savedCameras = GetSavedCameras(); 901 | sdMenuItems = new Dictionary>(); 902 | 903 | foreach (var sc in savedCameras) 904 | { 905 | MenuItem savedDroneBtn; 906 | if (sc.Key.Length > 4) 907 | { 908 | savedDroneBtn = new MenuItem(sc.Key.Substring(4), $"Manage this saved drone.") 909 | { 910 | Label = $"→→→" 911 | }; 912 | } 913 | else 914 | { 915 | savedDroneBtn = new MenuItem("NULL", $"Manage this saved drone.") 916 | { 917 | Label = $"→→→" 918 | }; 919 | } 920 | savedDronesMenu.AddMenuItem(savedDroneBtn); 921 | sdMenuItems.Add(savedDroneBtn, sc); 922 | } 923 | await Delay(0); 924 | } 925 | 926 | #endregion 927 | 928 | private static string _t(string key) { 929 | return Language.get(key); 930 | } 931 | } 932 | } -------------------------------------------------------------------------------- /EnhancedCamera/menus/CustomCam.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using CitizenFX.Core; 5 | using static CitizenFX.Core.Native.API; 6 | using MenuAPI; 7 | using Newtonsoft.Json; 8 | 9 | namespace CustomCamera 10 | { 11 | public static class CameraConstraints 12 | { 13 | public const float ROLL_MIN = (-50f); 14 | public const float ROLL_MAX = (50f); 15 | public const float PITCH_MIN = (-65f); 16 | public const float PITCH_MAX = (65f); 17 | 18 | public static float ClampRoll(float roll) 19 | { 20 | roll = (roll < ROLL_MIN) ? (ROLL_MIN) : (roll); 21 | roll = (roll > ROLL_MAX) ? (ROLL_MAX) : (roll); 22 | return roll; 23 | } 24 | public static float ClampPitch(float pitch) 25 | { 26 | pitch = (pitch < PITCH_MIN) ? (PITCH_MIN) : (pitch); 27 | pitch = (pitch > PITCH_MAX) ? (PITCH_MAX) : (pitch); 28 | return pitch; 29 | } 30 | public static bool OverClampCheck(float roll, float pitch) 31 | { 32 | return ((roll < ROLL_MIN) || 33 | (roll > ROLL_MAX) || 34 | (pitch < PITCH_MIN) || 35 | (pitch > PITCH_MAX)); 36 | } 37 | public static bool CrashCheck(int veh) 38 | { 39 | return ((GetEntityRoll(veh) < ROLL_MIN) || 40 | (GetEntityRoll(veh) > ROLL_MAX) || 41 | (GetEntityPitch(veh) < PITCH_MIN) || 42 | (GetEntityPitch(veh) > PITCH_MAX)); 43 | } 44 | } 45 | 46 | public class CustomCam : BaseScript 47 | { 48 | #region variables 49 | 50 | // Public variables 51 | public static bool LeadCam = false; 52 | public static bool ChaseCam = false; 53 | 54 | // Private variables 55 | private Menu menu; 56 | private Dictionary> scMenuItems = new Dictionary>(); 57 | private Menu savedCamerasMenu; 58 | private Menu selectedCameraMenu = new Menu(_t("CUSTOM_CAM_MANAGE"), _t("CUSTOM_CAM_MANAGE_DESC")); 59 | private static KeyValuePair currentlySelectedCamera = new KeyValuePair(); 60 | 61 | #endregion 62 | 63 | #region GUI updating 64 | 65 | // GUI parameters 66 | private MenuCheckboxItem lockPosOffsetCheckbox; 67 | private MenuCheckboxItem linearPosCheckbox; 68 | private MenuCheckboxItem pedLockCheckbox; 69 | private MenuListItem angCamModifierList; 70 | private MenuListItem angCamInterpolationList; 71 | private MenuListItem rollInterpolationList; 72 | private MenuListItem pitchInterpolationList; 73 | private MenuListItem chaseCamOffsetList; 74 | private MenuListItem posInterpolationList; 75 | private MenuListItem customCamFOVList; 76 | private MenuListItem customCamForwardOffsetList; 77 | private MenuListItem customCamSideOffsetList; 78 | private MenuListItem customCamUpOffsetList; 79 | private MenuListItem chaseCamMaxAngleList; 80 | 81 | // Update params 82 | private void UpdateParams() 83 | { 84 | // Reset camera to update params 85 | MainMenu.ResetCameras(); 86 | if (LeadCam) 87 | { 88 | MainMenu.driftCamera = MainMenu.CreateNonAttachedCamera(); 89 | MainMenu.driftCamera.IsActive = true; 90 | } 91 | else if (ChaseCam) 92 | { 93 | MainMenu.chaseCamera = MainMenu.CreateNonAttachedCamera(); 94 | MainMenu.chaseCamera.IsActive = true; 95 | } 96 | // Update GUI params 97 | angCamModifierList.ListIndex = (int)((angCamModifier - 0.0001f + 1f) / 0.025f); 98 | angCamInterpolationList.ListIndex = (int)((angCamInterpolation) / 0.005f); 99 | chaseCamOffsetList.ListIndex = (chaseCamOffset); 100 | posInterpolationList.ListIndex = (int)((posInterpolation) / 0.01f); 101 | rollInterpolationList.ListIndex = (int)((cameraRollInterpolation) / 0.005f); 102 | pitchInterpolationList.ListIndex = (int)((cameraPitchInterpolation) / 0.005f); 103 | chaseCamMaxAngleList.ListIndex = (int)((maxAngle - 25f) / 5f); 104 | customCamFOVList.ListIndex = (int)(fov - 20.0f); 105 | customCamForwardOffsetList.ListIndex = (int)((forwardOffset + 8f) / 0.025f); 106 | customCamUpOffsetList.ListIndex = (int)((upOffset + 5f) / 0.025f); 107 | customCamSideOffsetList.ListIndex = (int)((sideOffset + 5f) / 0.025f); 108 | lockPosOffsetCheckbox.Checked = lockOffsetPos; 109 | linearPosCheckbox.Checked = linearPosOffset; 110 | pedLockCheckbox.Checked = pedLock; 111 | 112 | menu.RefreshIndex(); 113 | } 114 | 115 | #endregion 116 | 117 | // Constructor 118 | public CustomCam() 119 | { 120 | Tick += RunDriftCam; 121 | Tick += RunChaseCam; 122 | } 123 | 124 | private void CreateMenu() 125 | { 126 | menu = new Menu(_t("CUSTOM_CAM_TITLE"), _t("CUSTOM_CAM_DESC")); 127 | 128 | #region checkbox items 129 | 130 | // Lock position offset 131 | lockPosOffsetCheckbox = new MenuCheckboxItem(_t("CUSTOM_CAM_LOCK_POSITION"), _t("CUSTOM_CAM_LOCK_POSITION_DESC"), false); 132 | // Linear position offset 133 | linearPosCheckbox = new MenuCheckboxItem(_t("CUSTOM_CAM_LINEAR"), _t("CUSTOM_CAM_LINEAR_DESC"), false); 134 | // Lock to ped 135 | pedLockCheckbox = new MenuCheckboxItem(_t("CUSTOM_CAM_LOCK_ROTATE"), _t("CUSTOM_CAM_LOCK_ROTATE_DESC"), false); 136 | 137 | #endregion 138 | 139 | #region main parameters 140 | 141 | // Angular velocity modifier 142 | List angCamModifierValues = new List(); 143 | for (float i = -1f; i < 1f; i += 0.025f) 144 | { 145 | angCamModifierValues.Add(i.ToString("0.000")); 146 | } 147 | angCamModifierList = new MenuListItem(_t("CUSTOM_CAM_MODIFIER"), angCamModifierValues, 48, _t("CUSTOM_CAM_MODIFIER_DESC")) 148 | { 149 | ShowColorPanel = false 150 | }; 151 | 152 | // Yaw interpolation modifier 153 | List angCamInterpolationValues = new List(); 154 | for (float i = 0.0f; i < 1f; i += 0.005f) 155 | { 156 | angCamInterpolationValues.Add(i.ToString("0.000")); 157 | } 158 | angCamInterpolationList = new MenuListItem(_t("CUSTOM_CAM_YAW"), angCamInterpolationValues, 4, _t("CUSTOM_CAM_YAW_DESC")) 159 | { 160 | ShowColorPanel = false 161 | }; 162 | 163 | // Roll interpolation modifier 164 | List rollInterpolationValues = new List(); 165 | for (float i = 0.0f; i < 1f; i += 0.005f) 166 | { 167 | rollInterpolationValues.Add(i.ToString("0.000")); 168 | } 169 | rollInterpolationList = new MenuListItem(_t("CUSTOM_CAM_ROLL"), rollInterpolationValues, 20, _t("CUSTOM_CAM_ROLL_DESC")) 170 | { 171 | ShowColorPanel = false 172 | }; 173 | 174 | // Roll interpolation modifier 175 | List pitchInterpolationValues = new List(); 176 | for (float i = 0.0f; i < 1f; i += 0.005f) 177 | { 178 | pitchInterpolationValues.Add(i.ToString("0.000")); 179 | } 180 | pitchInterpolationList = new MenuListItem(_t("CUSTOM_CAM_PITCH"), pitchInterpolationValues, 20, _t("CUSTOM_CAM_PITCH_DESC")) 181 | { 182 | ShowColorPanel = false 183 | }; 184 | 185 | // Chase cam offset modifier 186 | List chaseCamOffsetValues = new List(); 187 | for (float i = 0; i <= 5; i += 0.125f) 188 | { 189 | chaseCamOffsetValues.Add((i).ToString("0.000")); 190 | } 191 | chaseCamOffsetList = new MenuListItem(_t("CUSTOM_CAM_OFFSET"), chaseCamOffsetValues, 0, _t("CUSTOM_CAM_OFFSET_DESC")) 192 | { 193 | ShowColorPanel = false 194 | }; 195 | 196 | // Camera x position offset interpolation modifier 197 | List posInterpolationValues = new List(); 198 | for (float i = 0.0f; i < 1f; i += 0.01f) 199 | { 200 | posInterpolationValues.Add(i.ToString("0.00")); 201 | } 202 | posInterpolationList = new MenuListItem(_t("CUSTOM_CAM_POSITION"), posInterpolationValues, 100, _t("CUSTOM_CAM_POSITION_DESC")) 203 | { 204 | ShowColorPanel = false 205 | }; 206 | 207 | // FOV modifier 208 | List customCamFOVValues = new List(); 209 | for (float i = 20; i <= 120; i += 1f) 210 | { 211 | customCamFOVValues.Add((i).ToString()); 212 | } 213 | customCamFOVList = new MenuListItem(_t("CUSTOM_CAM_FOV"), customCamFOVValues, 43, _t("CUSTOM_CAM_FOV_DESC")) 214 | { 215 | ShowColorPanel = false 216 | }; 217 | 218 | // Custom cam forward offset 219 | List customCamForwardOffsetValues = new List(); 220 | for (float i = -8; i <= 8; i += 0.025f) 221 | { 222 | customCamForwardOffsetValues.Add((i).ToString("0.000")); 223 | } 224 | customCamForwardOffsetList = new MenuListItem(_t("CUSTOM_CAM_Y_OFFSET"), customCamForwardOffsetValues, 130, _t("CUSTOM_CAM_Y_OFFSET_DESC")) 225 | { 226 | ShowColorPanel = false 227 | }; 228 | // Custom cam side offset 229 | List customCamSideOffsetValues = new List(); 230 | for (float i = -5; i <= 8; i += 0.025f) 231 | { 232 | customCamSideOffsetValues.Add((i).ToString("0.000")); 233 | } 234 | customCamSideOffsetList = new MenuListItem(_t("CUSTOM_CAM_X_OFFSET"), customCamSideOffsetValues, 200, _t("CUSTOM_CAM_X_OFFSET_DESC")) 235 | { 236 | ShowColorPanel = false 237 | }; 238 | // Custom cam up offset 239 | List customCamUpOffsetValues = new List(); 240 | for (float i = -5; i <= 8; i += 0.025f) 241 | { 242 | customCamUpOffsetValues.Add((i).ToString("0.000")); 243 | } 244 | customCamUpOffsetList = new MenuListItem(_t("CUSTOM_CAM_Z_OFFSET"), customCamUpOffsetValues, 282, _t("CUSTOM_CAM_Z_OFFSET_DESC")) 245 | { 246 | ShowColorPanel = false 247 | }; 248 | 249 | List chaseCamMaxAngleValues = new List(); 250 | for (float i = 25; i <= 360; i += 5) 251 | { 252 | chaseCamMaxAngleValues.Add(i.ToString()); 253 | } 254 | chaseCamMaxAngleList = new MenuListItem(_t("CUSTOM_CAM_MAX_ANGLE"), chaseCamMaxAngleValues, 67, _t("CUSTOM_CAM_MAX_ANGLE_DESC")) 255 | { 256 | ShowColorPanel = false 257 | }; 258 | 259 | #endregion 260 | 261 | #region adding menu items 262 | // Checkboxes 263 | menu.AddMenuItem(lockPosOffsetCheckbox); 264 | menu.AddMenuItem(linearPosCheckbox); 265 | menu.AddMenuItem(pedLockCheckbox); 266 | // Main modifier 267 | menu.AddMenuItem(angCamModifierList); 268 | // Interpolation sliders 269 | menu.AddMenuItem(angCamInterpolationList); 270 | menu.AddMenuItem(rollInterpolationList); 271 | menu.AddMenuItem(pitchInterpolationList); 272 | menu.AddMenuItem(posInterpolationList); 273 | // Chase camera 274 | menu.AddMenuItem(chaseCamOffsetList); 275 | menu.AddMenuItem(chaseCamMaxAngleList); 276 | // FOV and offset 277 | menu.AddMenuItem(customCamFOVList); 278 | menu.AddMenuItem(customCamForwardOffsetList); 279 | menu.AddMenuItem(customCamUpOffsetList); 280 | menu.AddMenuItem(customCamSideOffsetList); 281 | 282 | // Presets 283 | Menu presetsMenu = new Menu(_t("CUSTOM_CAM_PRESETS"), _t("CUSTOM_CAM_PRESETS_DESC")); 284 | MenuItem tandemCamPreset = new MenuItem(_t("CUSTOM_CAM_PRESETS_TANDEM"), _t("CUSTOM_CAM_PRESETS_TANDEM_DESC")) 285 | { 286 | Label = $"→→→" 287 | }; 288 | MenuItem fpvCamPreset = new MenuItem(_t("CUSTOM_CAM_PRESETS_FPV"), _t("CUSTOM_CAM_PRESETS_FPV_DESC")) 289 | { 290 | Label = $"→→→" 291 | }; 292 | MenuItem NFSCamPreset = new MenuItem(_t("CUSTOM_CAM_PRESETS_NFS"), _t("CUSTOM_CAM_PRESETS_NFS_DESC")) 293 | { 294 | Label = $"→→→" 295 | }; 296 | presetsMenu.AddMenuItem(tandemCamPreset); 297 | presetsMenu.AddMenuItem(fpvCamPreset); 298 | presetsMenu.AddMenuItem(NFSCamPreset); 299 | 300 | presetsMenu.OnItemSelect += (sender, item, index) => { 301 | if (item == tandemCamPreset) 302 | { 303 | MainMenu.Notify("~g~~h~Info~h~~s~: Switching to Tandem Camera 1.0. Tune XYZ offsets to your car."); 304 | 305 | currentlySelectedCamera = new KeyValuePair("_1__", CustomCamPresets.tandemCam1); 306 | SpawnSavedCamera(); 307 | 308 | // Update menu stuff according to loaded values 309 | UpdateParams(); 310 | presetsMenu.GoBack(); 311 | } 312 | if (item == fpvCamPreset) 313 | { 314 | MainMenu.Notify("~g~~h~Info~h~~s~: Switching to FPV camera base. Tune XYZ offsets to your car."); 315 | 316 | currentlySelectedCamera = new KeyValuePair("_2__", CustomCamPresets.fpvCam1); 317 | SpawnSavedCamera(); 318 | 319 | // Update menu stuff according to loaded values 320 | UpdateParams(); 321 | presetsMenu.GoBack(); 322 | } 323 | if (item == NFSCamPreset) 324 | { 325 | MainMenu.Notify("~g~~h~Info~h~~s~: Switching to NFS camera. Tune XYZ offsets to your car."); 326 | 327 | currentlySelectedCamera = new KeyValuePair("_3__", CustomCamPresets.NFSCam); 328 | SpawnSavedCamera(); 329 | 330 | // Update menu stuff according to loaded values 331 | UpdateParams(); 332 | presetsMenu.GoBack(); 333 | } 334 | }; 335 | 336 | MenuItem buttonPresets = new MenuItem(_t("CUSTOM_CAM_PRESETS_MENU"), _t("CUSTOM_CAM_PRESETS_MENU_DESC")) 337 | { 338 | Label = "→→→" 339 | }; 340 | menu.AddMenuItem(buttonPresets); 341 | MenuController.AddSubmenu(menu, presetsMenu); 342 | MenuController.BindMenuItem(menu, presetsMenu, buttonPresets); 343 | presetsMenu.RefreshIndex(); 344 | 345 | #endregion 346 | 347 | #region managing save/load camera stuff 348 | 349 | // Saving/Loading cameras 350 | MenuItem savedCamerasButton = new MenuItem(_t("CUSTOM_CAM_SAVED"), _t("CUSTOM_CAM_SAVED_DESC")); 351 | savedCamerasMenu = new Menu("Saved cameras"); 352 | MenuController.AddSubmenu(menu, savedCamerasMenu); 353 | menu.AddMenuItem(savedCamerasButton); 354 | savedCamerasButton.Label = "→→→"; 355 | MenuController.BindMenuItem(menu, savedCamerasMenu, savedCamerasButton); 356 | 357 | MenuItem saveCamera = new MenuItem(_t("CUSTOM_CAM_SAVE_CURRENT"), _t("CUSTOM_CAM_SAVE_CURRENT_DESC")); 358 | savedCamerasMenu.AddMenuItem(saveCamera); 359 | savedCamerasMenu.OnMenuOpen += (sender) => { 360 | savedCamerasMenu.ClearMenuItems(); 361 | savedCamerasMenu.AddMenuItem(saveCamera); 362 | LoadCameras(); 363 | }; 364 | 365 | savedCamerasMenu.OnItemSelect += (sender, item, index) => { 366 | if (item == saveCamera) 367 | { 368 | if (Game.PlayerPed.IsInVehicle()) 369 | { 370 | SaveCamera(); 371 | savedCamerasMenu.GoBack(); 372 | } 373 | else 374 | { 375 | MainMenu.Notify("~g~~h~Info~h~~s~: You are currently not in any vehicle. Please enter a vehicle before trying to save the camera."); 376 | } 377 | } 378 | else 379 | { 380 | UpdateSelectedCameraMenu(item, sender); 381 | } 382 | }; 383 | 384 | MenuController.AddMenu(selectedCameraMenu); 385 | MenuItem spawnCamera = new MenuItem(_t("CUSTOM_CAM_SPAWN"), _t("CUSTOM_CAM_SPAWN_DESC")); 386 | MenuItem renameCamera = new MenuItem(_t("CUSTOM_CAM_RENAME"), _t("CUSTOM_CAM_RENAME_DESC")); 387 | MenuItem deleteCamera = new MenuItem(_t("CUSTOM_CAM_DELETE"), _t("CUSTOM_CAM_DELETE_DESC")); 388 | selectedCameraMenu.AddMenuItem(spawnCamera); 389 | selectedCameraMenu.AddMenuItem(renameCamera); 390 | selectedCameraMenu.AddMenuItem(deleteCamera); 391 | 392 | selectedCameraMenu.OnMenuClose += (sender) => { 393 | selectedCameraMenu.RefreshIndex(); 394 | }; 395 | 396 | selectedCameraMenu.OnItemSelect += async (sender, item, index) => { 397 | if (item == spawnCamera) 398 | { 399 | MainMenu.ResetCameras(); 400 | SpawnSavedCamera(); 401 | UpdateParams(); 402 | selectedCameraMenu.GoBack(); 403 | savedCamerasMenu.RefreshIndex(); 404 | 405 | } 406 | else if (item == deleteCamera) 407 | { 408 | item.Label = ""; 409 | DeleteResourceKvp(currentlySelectedCamera.Key); 410 | selectedCameraMenu.GoBack(); 411 | savedCamerasMenu.RefreshIndex(); 412 | MainMenu.Notify("~g~~h~Info~h~~s~: Your saved camera has been deleted."); 413 | } 414 | else if (item == renameCamera) 415 | { 416 | string newName = await MainMenu.GetUserInput(windowTitle: "Enter a new name for this camera.", defaultText: null, maxInputLength: 30); 417 | if (string.IsNullOrEmpty(newName)) 418 | { 419 | MainMenu.Notify("~r~~h~Error~h~~s~: Invalid input"); 420 | } 421 | else 422 | { 423 | if (SaveCameraInfo("xcm_" + newName, currentlySelectedCamera.Value, false)) 424 | { 425 | DeleteResourceKvp(currentlySelectedCamera.Key); 426 | while (!selectedCameraMenu.Visible) 427 | { 428 | await BaseScript.Delay(0); 429 | } 430 | MainMenu.Notify("~g~~h~Info~h~~s~: Your camera has successfully been renamed."); 431 | selectedCameraMenu.GoBack(); 432 | currentlySelectedCamera = new KeyValuePair(); 433 | } 434 | else 435 | { 436 | MainMenu.Notify("~r~~h~Error~h~~s~: This name is already in use or something unknown failed. Contact the server owner if you believe something is wrong."); 437 | } 438 | } 439 | } 440 | }; 441 | 442 | #endregion 443 | 444 | #region handling menu changes 445 | 446 | // Handle checkbox 447 | menu.OnCheckboxChange += (_menu, _item, _index, _checked) => { 448 | if (_item == linearPosCheckbox) 449 | { 450 | linearPosOffset = _checked; 451 | } 452 | if (_item == lockPosOffsetCheckbox) 453 | { 454 | lockOffsetPos = _checked; 455 | } 456 | if (_item == pedLockCheckbox) 457 | { 458 | pedLock = _checked; 459 | } 460 | }; 461 | 462 | // Handle list change 463 | menu.OnListIndexChange += (_menu, _listItem, _oldIndex, _newIndex, _itemIndex) => { 464 | if (_listItem == angCamModifierList) 465 | { 466 | angCamModifier = _newIndex * 0.025f + 0.0001f - 1f; 467 | } 468 | if (_listItem == angCamInterpolationList) 469 | { 470 | angCamInterpolation = ((_newIndex) * 0.005f); 471 | } 472 | if (_listItem == chaseCamOffsetList) 473 | { 474 | chaseCamOffset = (_newIndex); 475 | } 476 | if (_listItem == posInterpolationList) 477 | { 478 | posInterpolation = ((_newIndex) * 0.01f); 479 | } 480 | if (_listItem == rollInterpolationList) 481 | { 482 | cameraRollInterpolation = ((_newIndex) * 0.005f); 483 | } 484 | if (_listItem == pitchInterpolationList) 485 | { 486 | cameraPitchInterpolation = ((_newIndex) * 0.005f); 487 | } 488 | if (_listItem == chaseCamMaxAngleList) 489 | { 490 | maxAngle = (float)(_newIndex * 5f + 25f); 491 | } 492 | if (_listItem == customCamFOVList) 493 | { 494 | fov = (float)(_newIndex * 1f + 20.0f); 495 | if (LeadCam) 496 | { 497 | SetCamFov(MainMenu.driftCamera.Handle, fov); 498 | } 499 | else if (ChaseCam) 500 | { 501 | SetCamFov(MainMenu.chaseCamera.Handle, fov); 502 | } 503 | } 504 | if (_listItem == customCamForwardOffsetList) 505 | { 506 | forwardOffset = (float)(_newIndex * 0.025f - 8f); 507 | } 508 | if (_listItem == customCamSideOffsetList) 509 | { 510 | sideOffset = (float)(_newIndex * 0.025f - 5f); 511 | } 512 | if (_listItem == customCamUpOffsetList) 513 | { 514 | upOffset = (float)(_newIndex * 0.025f - 5f); 515 | } 516 | }; 517 | 518 | #endregion 519 | 520 | } 521 | 522 | /// 523 | /// Creates the menu if it doesn't exist, and then returns it. 524 | /// 525 | /// The Menu 526 | public Menu GetMenu() 527 | { 528 | if (menu == null) 529 | { 530 | CreateMenu(); 531 | } 532 | return menu; 533 | } 534 | 535 | #region custom camera static variables 536 | 537 | public static float fov = 63.0f; 538 | private static float forwardOffset = -4.75f; 539 | private static float sideOffset = 0.0f; 540 | private static float upOffset = 2.05f; 541 | private static bool linearPosOffset = false; 542 | private static bool lockOffsetPos = false; 543 | private static float angCamModifier = 0.2f; 544 | private static float angCamInterpolation = 0.02f; 545 | private static float angularVelOld = 0f; 546 | private static float posInterpolation = 0.5f; 547 | private static float oldPosXOffset = 0f; 548 | public static float maxAngle = 360f; 549 | 550 | private static float cameraRollInterpolation = 0.1f; 551 | private static float cameraPitchInterpolation = 0.1f; 552 | 553 | private static bool pedLock = false; 554 | 555 | #endregion 556 | 557 | #region drift camera 558 | 559 | // Consts 560 | private const float MAX_ANG_VEL_OFFSET = 1.0f; 561 | private const float ROTATION_NORMALIZE = 100.0f; 562 | private const float TIMESTEP_DELIMITER = 0.015f; 563 | 564 | /// 565 | /// Changes main render camera behaviour, follows car with specified degree of freedom 566 | /// based on modifier value and interpolation value (and other variables such as 567 | /// angle and position offset values). 568 | /// 569 | /// 570 | private async Task RunDriftCam() 571 | { 572 | if (LeadCam) 573 | { 574 | int vehicleEntity = GetVehiclePedIsIn(PlayerPedId(), false); 575 | if (vehicleEntity > 0) 576 | { 577 | if (MainMenu.driftCamera != null) 578 | { 579 | // Calculate timestep to account for framerate drops 580 | //float deltaTime = Timestep() / CustomCam.TIMESTEP_DELIMITER; 581 | // Get vehicle's angular velocity 582 | float angularVel = GetEntityRotationVelocity(vehicleEntity).Z; 583 | // Keep it in reasonable range 584 | angularVel = (angularVel > MAX_ANG_VEL_OFFSET) ? (MAX_ANG_VEL_OFFSET) : (angularVel); 585 | // Lerp to smooth the camera transition 586 | angularVel = MainMenu.CamMath.Lerp(angularVelOld, angularVel, angCamInterpolation); 587 | // Save the value to lerp with it in the next frame 588 | angularVelOld = angularVel; 589 | // Calculating target camera rotation 590 | float finalRotation = -angularVel * angCamModifier * ROTATION_NORMALIZE; 591 | 592 | // Get vehicle entity for further operations 593 | Vehicle veh = new Vehicle(vehicleEntity); 594 | 595 | // Setting the position offset also based on angular velocity 596 | if (!lockOffsetPos) 597 | { 598 | oldPosXOffset = MainMenu.CamMath.Lerp(oldPosXOffset, finalRotation, (posInterpolation >= 1f) ? (0.99f) : (posInterpolation)); 599 | } 600 | else 601 | { 602 | oldPosXOffset = finalRotation; 603 | } 604 | 605 | // Get the static offset based on user's input 606 | Vector3 staticPosition = Vector3.Zero; 607 | if (pedLock) 608 | { 609 | staticPosition = veh.ForwardVector * forwardOffset + 610 | veh.RightVector * sideOffset + 611 | Vector3.ForwardLH * upOffset; 612 | } 613 | else 614 | { 615 | staticPosition = veh.ForwardVector * forwardOffset + 616 | veh.RightVector * sideOffset + 617 | veh.UpVector * upOffset; 618 | } 619 | 620 | // Calculate final offset taking into consideration dynamic offset (oldPosXOffset), static 621 | // offset and the offset resulting from rotating the camera around the car 622 | if (!linearPosOffset) 623 | { 624 | if (oldPosXOffset != finalRotation) 625 | { 626 | float rotation = oldPosXOffset + MainMenu.userYaw; 627 | if (pedLock) 628 | { 629 | MainMenu.driftCamera.Position = veh.Position + MainMenu.CamMath.RotateAroundAxis(staticPosition, Vector3.ForwardLH, rotation * MainMenu.CamMath.DegToRad); 630 | } 631 | else 632 | { 633 | MainMenu.driftCamera.Position = veh.Position + MainMenu.CamMath.RotateAroundAxis(staticPosition, veh.UpVector, rotation * MainMenu.CamMath.DegToRad); 634 | } 635 | if (MainMenu.userLookBehind) 636 | { 637 | MainMenu.driftCamera.Position = veh.Position + 638 | MainMenu.CamMath.RotateAroundAxis(staticPosition, veh.UpVector, 179f * MainMenu.CamMath.DegToRad); 639 | } 640 | } 641 | else 642 | { 643 | if (MainMenu.userLookBehind) 644 | { 645 | MainMenu.driftCamera.Position = veh.Position + 646 | staticPosition - 647 | (veh.RightVector * sideOffset) + 648 | veh.ForwardVector * 3.5f + 649 | veh.UpVector * 0.5f; 650 | } 651 | else 652 | { 653 | MainMenu.driftCamera.Position = veh.Position + staticPosition; 654 | } 655 | } 656 | } 657 | else 658 | { 659 | MainMenu.driftCamera.Position = veh.Position + staticPosition + veh.RightVector * oldPosXOffset / 12f; 660 | if (MainMenu.userLookBehind) 661 | { 662 | MainMenu.driftCamera.Position = veh.Position + MainMenu.CamMath.RotateAroundAxis(staticPosition, veh.UpVector, 179f * MainMenu.CamMath.DegToRad); 663 | } 664 | } 665 | 666 | // Calculate target rotation as a heading in given range 667 | Vector3 newRot = GameMath.DirectionToRotation(GameMath.HeadingToDirection((oldPosXOffset + GetEntityRotation(vehicleEntity, 2).Z + 180.0f) % 360.0f - 180.0f), GetEntityRoll(vehicleEntity)); 668 | float roll = 0f; 669 | float pitch = 0f; 670 | // Clamp values 671 | if (CameraConstraints.CrashCheck(vehicleEntity)) 672 | { 673 | staticPosition = Vector3.ForwardLH * upOffset; 674 | MainMenu.driftCamera.Position = veh.Position + staticPosition; 675 | roll = MainMenu.CamMath.Lerp(MainMenu.driftCamera.Rotation.Y, 0f, 0.1f); 676 | pitch = MainMenu.CamMath.Lerp(MainMenu.driftCamera.Rotation.X, 0f, 0.1f); 677 | pitch = CameraConstraints.ClampPitch(pitch); 678 | } 679 | else 680 | { 681 | // Calculate smooth roll and pitch rotation 682 | roll = MainMenu.CamMath.Lerp(MainMenu.driftCamera.Rotation.Y, -GetEntityRoll(vehicleEntity), cameraRollInterpolation); 683 | pitch = MainMenu.CamMath.Lerp(MainMenu.driftCamera.Rotation.X - MainMenu.userTilt, GetEntityRotation(vehicleEntity, 2).X, cameraPitchInterpolation); 684 | roll = CameraConstraints.ClampRoll(roll); 685 | pitch = CameraConstraints.ClampPitch(pitch); 686 | } 687 | // Finalize the rotation 688 | float yaw = (MainMenu.userLookBehind) ? (GetEntityRotation(vehicleEntity, 2).Z + 179.9f) : (newRot.Z + MainMenu.userYaw); 689 | SetCamRot(MainMenu.driftCamera.Handle, pitch + MainMenu.userTilt, roll, yaw, 2); 690 | 691 | // Update minimap 692 | LockMinimapAngle((int)(MainMenu.CamMath.Fmod(yaw, 360f))); 693 | } 694 | else 695 | { 696 | // In case the camera is null - reset the cameras and reassign this camera 697 | MainMenu.ResetCameras(); 698 | MainMenu.driftCamera = MainMenu.CreateNonAttachedCamera(); 699 | MainMenu.driftCamera.IsActive = true; 700 | } 701 | } 702 | else 703 | { 704 | // Disable custom camera 705 | if (LeadCam || ChaseCam) 706 | { 707 | MainMenu.SwitchToGameplayCam(); 708 | MainMenu.Notify("~g~~h~Info~h~~s~: Vehicle not found, switching to gameplay camera..."); 709 | } 710 | } 711 | } 712 | else 713 | { 714 | await Delay(0); 715 | } 716 | } 717 | 718 | #endregion 719 | 720 | #region chase camera 721 | 722 | /// 723 | /// Gets closest vehicle to Ped 724 | /// 725 | /// closest vehicle 726 | public static Vehicle GetClosestVehicle(int maxDistance, float requiredAngle) 727 | { 728 | float smallestDistance = (float)maxDistance; 729 | Vehicle[] vehs = World.GetAllVehicles(); 730 | Vehicle closestVeh = null; 731 | 732 | int playerVeh = GetVehiclePedIsIn(PlayerPedId(), false); 733 | 734 | if (vehs != null) 735 | { 736 | foreach (Vehicle veh in vehs) 737 | { 738 | if (veh.Handle != playerVeh) 739 | { 740 | float distance = Vector3.Distance(GetEntityCoords(veh.Handle, true), GetEntityCoords(playerVeh, true)); 741 | if ((distance <= smallestDistance) && (veh != null)) 742 | { 743 | smallestDistance = distance; 744 | Vector3 targetVec = GetOffsetFromEntityGivenWorldCoords(playerVeh, veh.Position.X, veh.Position.Y, veh.Position.Z); 745 | float angle = -MainMenu.CamMath.AngleBetween(targetVec, new Vector3(0, 0.0001f, 0) + GetEntitySpeedVector(playerVeh, true)); 746 | // Make sure that target is in range given by angle 747 | if (Math.Abs(angle) < requiredAngle) 748 | { 749 | closestVeh = veh; 750 | } 751 | } 752 | } 753 | } 754 | } 755 | return closestVeh; 756 | } 757 | 758 | public static Vehicle target = null; 759 | private static int chaseCamOffset = 0; 760 | 761 | /// 762 | /// Changes main render camera behaviour, camera locks onto closest vehicle 763 | /// that is in front of the player (in certain degree range in front of car's 764 | /// velocity's magnitude). 765 | /// 766 | /// 767 | private async Task RunChaseCam() 768 | { 769 | if (ChaseCam) 770 | { 771 | // Get player's vehicle 772 | int vehicleEntity = GetVehiclePedIsIn(PlayerPedId(), false); 773 | if (vehicleEntity > 0) 774 | { 775 | if (MainMenu.chaseCamera != null) 776 | { 777 | 778 | // If target car is located 779 | if (target != null) 780 | { 781 | // Get vector from player's car to target car offset by value 782 | Vector3 targetVec = GetOffsetFromEntityGivenWorldCoords( 783 | vehicleEntity, 784 | target.Position.X + target.ForwardVector.X * (chaseCamOffset / 5), 785 | target.Position.Y + target.ForwardVector.Y * (chaseCamOffset / 5), 786 | target.Position.Z); 787 | 788 | // Get rotation to target vehicle 789 | float finalRotation = -MainMenu.CamMath.AngleBetween(targetVec, new Vector3(0, 10, 0)); 790 | 791 | if (Math.Abs(finalRotation) > maxAngle) 792 | { 793 | target = null; 794 | MainMenu.SwitchCameraToDrift(); 795 | MainMenu.Notify("~g~~h~Info~h~~s~: Target exceeded angle limit, switching to Lead Camera"); 796 | return; 797 | } 798 | 799 | if (finalRotation.ToString() != "NaN") 800 | { 801 | // Lerp target rotation 802 | // (1 - angCamInterpolation) instead of just interpolation so that camera 803 | // can be changed smoothly from lead cam to chase cam 804 | finalRotation = MainMenu.CamMath.Lerp(GetEntityHeading(MainMenu.chaseCamera.Handle), finalRotation, 1 - angCamInterpolation); 805 | 806 | // Calculate camera's position 807 | Vehicle veh = new Vehicle(vehicleEntity); 808 | 809 | // Static position as an offset from the car 810 | Vector3 staticPosition = Vector3.Zero; 811 | if (pedLock) 812 | { 813 | staticPosition = veh.ForwardVector * forwardOffset + 814 | veh.RightVector * sideOffset + 815 | Vector3.ForwardLH * upOffset; 816 | } 817 | else 818 | { 819 | staticPosition = veh.ForwardVector * forwardOffset + 820 | veh.RightVector * sideOffset + 821 | veh.UpVector * upOffset; 822 | } 823 | 824 | // Calculate chase camera position 825 | if (!lockOffsetPos) 826 | { 827 | float rotation = finalRotation + MainMenu.userYaw; 828 | if (pedLock) 829 | { 830 | MainMenu.chaseCamera.Position = veh.Position + MainMenu.CamMath.RotateAroundAxis(staticPosition, Vector3.ForwardLH, rotation * MainMenu.CamMath.DegToRad); 831 | } 832 | else 833 | { 834 | MainMenu.chaseCamera.Position = veh.Position + MainMenu.CamMath.RotateAroundAxis(staticPosition, veh.UpVector, rotation * MainMenu.CamMath.DegToRad); 835 | } 836 | if (MainMenu.userLookBehind) { MainMenu.chaseCamera.Position = veh.Position - (veh.RightVector * sideOffset) + MainMenu.CamMath.RotateAroundAxis(staticPosition, veh.UpVector, 179f * MainMenu.CamMath.DegToRad); } 837 | } 838 | else 839 | { 840 | MainMenu.chaseCamera.Position = veh.Position + staticPosition; 841 | if (MainMenu.userLookBehind) { MainMenu.chaseCamera.Position = veh.Position + staticPosition + veh.ForwardVector * 3f + veh.UpVector * 0.5f; } 842 | } 843 | 844 | // Calculate the camera rotation 845 | Vector3 newRot = GameMath.DirectionToRotation(GameMath.HeadingToDirection((finalRotation + GetEntityRotation(vehicleEntity, 4).Z + 180.0f) % 360.0f - 180.0f), GetEntityRoll(vehicleEntity)); 846 | 847 | // Calculate smooth roll and pitch rotation 848 | float roll = 0f; 849 | float pitch = 0f; 850 | // Clamp values 851 | if (CameraConstraints.CrashCheck(vehicleEntity)) 852 | { 853 | staticPosition = Vector3.ForwardLH * upOffset; 854 | MainMenu.chaseCamera.Position = veh.Position + staticPosition; 855 | roll = MainMenu.CamMath.Lerp(MainMenu.chaseCamera.Rotation.Y, 0f, 0.1f); 856 | pitch = MainMenu.CamMath.Lerp(MainMenu.chaseCamera.Rotation.X, 0f, 0.1f); 857 | pitch = CameraConstraints.ClampPitch(pitch); 858 | } 859 | else 860 | { 861 | // Calculate smooth roll and pitch rotation 862 | roll = MainMenu.CamMath.Lerp(MainMenu.chaseCamera.Rotation.Y, -GetEntityRoll(vehicleEntity), cameraRollInterpolation); 863 | pitch = MainMenu.CamMath.Lerp(MainMenu.chaseCamera.Rotation.X - MainMenu.userTilt, GetEntityPitch(vehicleEntity), cameraPitchInterpolation); 864 | roll = CameraConstraints.ClampRoll(roll); 865 | pitch = CameraConstraints.ClampPitch(pitch); 866 | } 867 | // Finally, set the rotation 868 | float yaw = (MainMenu.userLookBehind) ? (GetEntityRotation(vehicleEntity, 2).Z + 179.9f) : (newRot.Z + MainMenu.userYaw); 869 | MainMenu.chaseCamera.Rotation = new Vector3(pitch + MainMenu.userTilt, roll, yaw); 870 | 871 | // Update minimap 872 | LockMinimapAngle((int)(MainMenu.CamMath.Fmod(yaw, 360f))); 873 | } 874 | } 875 | else 876 | { 877 | // Target car not found - switch to Lead Cam 878 | MainMenu.SwitchCameraToDrift(); 879 | MainMenu.Notify("~g~~h~Info~h~~s~: Target not found, switching to Lead Camera"); 880 | } 881 | 882 | // Find target and generate camera 883 | } 884 | else 885 | { 886 | MainMenu.ResetCameras(); 887 | MainMenu.chaseCamera = MainMenu.CreateNonAttachedCamera(); 888 | MainMenu.chaseCamera.IsActive = true; 889 | target = GetClosestVehicle(2000, maxAngle); 890 | } 891 | } 892 | else 893 | { 894 | // Disable custom camera 895 | if (LeadCam || ChaseCam) 896 | { 897 | MainMenu.SwitchToGameplayCam(); 898 | MainMenu.Notify("~g~~h~Info~h~~s~: Vehicle not found, switching to gameplay camera..."); 899 | } 900 | } 901 | } 902 | else 903 | { 904 | await Delay(0); 905 | } 906 | } 907 | 908 | #endregion 909 | 910 | /// --- 911 | /// Save/load functions originally made by Vespura (https://www.tomgrobbe.com/) for vMenu. 912 | /// Snippets of the code were slightly modified to suit camera needs and added here. 913 | /// --- 914 | #region save/load 915 | 916 | public struct CameraInfo 917 | { 918 | public float angCamInterpolation_; 919 | public float angCamModifier_; 920 | public float posInterpolation_; 921 | public float chaseCamMaxAngle_; 922 | public bool linearPosOffset_; 923 | public bool lockOffsetPos_; 924 | public float customCamFOV_; 925 | public float customCamForwardOffset_; 926 | public float customCamUpOffset_; 927 | public float customCamSideOffset_; 928 | public float cameraRollInterpolation_; 929 | public float cameraPitchInterpolation_; 930 | public bool pedLock_; 931 | } 932 | 933 | private bool UpdateSelectedCameraMenu(MenuItem selectedItem, Menu parentMenu = null) 934 | { 935 | if (!scMenuItems.ContainsKey(selectedItem)) 936 | { 937 | MainMenu.Notify("~r~~h~Error~h~~s~: In some very strange way, you've managed to select a button, that does not exist according to this list. So your vehicle could not be loaded. :( Maybe your save files are broken?"); 938 | return false; 939 | } 940 | var camInfo = scMenuItems[selectedItem]; 941 | currentlySelectedCamera = camInfo; 942 | selectedCameraMenu.MenuSubtitle = $"{camInfo.Key.Substring(4)}"; 943 | MenuController.CloseAllMenus(); 944 | selectedCameraMenu.OpenMenu(); 945 | if (parentMenu != null) 946 | { 947 | MenuController.AddSubmenu(parentMenu, selectedCameraMenu); 948 | } 949 | return true; 950 | } 951 | 952 | private bool SpawnSavedCamera() 953 | { 954 | if (currentlySelectedCamera.Key != null) 955 | { 956 | angCamInterpolation = currentlySelectedCamera.Value.angCamInterpolation_; 957 | angCamModifier = currentlySelectedCamera.Value.angCamModifier_; 958 | posInterpolation = currentlySelectedCamera.Value.posInterpolation_; 959 | maxAngle = currentlySelectedCamera.Value.chaseCamMaxAngle_; 960 | linearPosOffset = currentlySelectedCamera.Value.linearPosOffset_; 961 | lockOffsetPos = currentlySelectedCamera.Value.lockOffsetPos_; 962 | fov = currentlySelectedCamera.Value.customCamFOV_; 963 | forwardOffset = currentlySelectedCamera.Value.customCamForwardOffset_; 964 | upOffset = currentlySelectedCamera.Value.customCamUpOffset_; 965 | sideOffset = currentlySelectedCamera.Value.customCamSideOffset_; 966 | cameraRollInterpolation = currentlySelectedCamera.Value.cameraRollInterpolation_; 967 | cameraPitchInterpolation = currentlySelectedCamera.Value.cameraPitchInterpolation_; 968 | pedLock = currentlySelectedCamera.Value.pedLock_; 969 | } 970 | else 971 | { 972 | MainMenu.Notify("~r~~h~Error~h~~s~: It seems that this slot got corrupted in some way, you need to delete it."); 973 | return false; 974 | } 975 | return true; 976 | } 977 | 978 | private bool SaveCameraInfo(string saveName, CameraInfo cameraInfo, bool overrideOldVersion) 979 | { 980 | if (string.IsNullOrEmpty(GetResourceKvpString(saveName)) || overrideOldVersion) 981 | { 982 | if (!string.IsNullOrEmpty(saveName) && saveName.Length > 4) 983 | { 984 | // convert 985 | string json = JsonConvert.SerializeObject(cameraInfo); 986 | 987 | // log 988 | Debug.WriteLine($"Saving!\nName: {saveName}\nCamera Data: {json}\n"); 989 | 990 | // save 991 | SetResourceKvp(saveName, json); 992 | 993 | // confirm 994 | return GetResourceKvpString(saveName) == json; 995 | } 996 | } 997 | // if something isn't right, then the save is aborted and return false ("failed" state). 998 | return false; 999 | } 1000 | 1001 | public async void SaveCamera(string updateExistingSavedCameraName = null) 1002 | { 1003 | // Only continue if the player is in a vehicle. 1004 | if (Game.PlayerPed.IsInVehicle()) 1005 | { 1006 | CameraInfo ci = new CameraInfo() 1007 | { 1008 | angCamInterpolation_ = angCamInterpolation, 1009 | angCamModifier_ = angCamModifier, 1010 | posInterpolation_ = posInterpolation, 1011 | chaseCamMaxAngle_ = maxAngle, 1012 | linearPosOffset_ = linearPosOffset, 1013 | lockOffsetPos_ = lockOffsetPos, 1014 | customCamFOV_ = fov, 1015 | customCamForwardOffset_ = forwardOffset, 1016 | customCamUpOffset_ = upOffset, 1017 | customCamSideOffset_ = sideOffset, 1018 | cameraRollInterpolation_ = cameraRollInterpolation, 1019 | cameraPitchInterpolation_ = cameraPitchInterpolation, 1020 | pedLock_ = pedLock 1021 | }; 1022 | 1023 | if (updateExistingSavedCameraName == null) 1024 | { 1025 | var saveName = await MainMenu.GetUserInput(windowTitle: "Enter a save name", defaultText: null, maxInputLength: 30); 1026 | // If the name is not invalid. 1027 | if (!string.IsNullOrEmpty(saveName)) 1028 | { 1029 | // Save everything from the dictionary into the client's kvp storage. 1030 | // If the save was successfull: 1031 | if (SaveCameraInfo("xcm_" + saveName, ci, false)) 1032 | { 1033 | MainMenu.Notify($"~g~~h~Info~h~~s~: Camera {saveName} saved."); 1034 | LoadCameras(); 1035 | } 1036 | // If the save was not successfull: 1037 | else 1038 | { 1039 | MainMenu.Notify("~r~~h~Error~h~~s~: Save already exists: (" + saveName + ")"); 1040 | } 1041 | } 1042 | // The user did not enter a valid name to use as a save name for this vehicle. 1043 | else 1044 | { 1045 | MainMenu.Notify("~r~~h~Error~h~~s~: Invalid save name"); 1046 | } 1047 | } 1048 | // We need to update an existing slot. 1049 | else 1050 | { 1051 | SaveCameraInfo("xcm_" + updateExistingSavedCameraName, ci, true); 1052 | } 1053 | } 1054 | // The player is not inside a vehicle. 1055 | else 1056 | { 1057 | MainMenu.Notify("~g~~h~Error~h~~s~: You need to be inside the vehicle"); 1058 | } 1059 | } 1060 | 1061 | private Dictionary GetSavedCameras() 1062 | { 1063 | // Create a list to store all saved camera names in. 1064 | var savedCameraNames = new List(); 1065 | // Start looking for kvps starting with xcm_ 1066 | var findHandle = StartFindKvp("xcm_"); 1067 | // Keep looking... 1068 | while (true) 1069 | { 1070 | // Get the kvp string key. 1071 | var camString = FindKvp(findHandle); 1072 | 1073 | // If it exists then the key to the list. 1074 | if (camString != "" && camString != null && camString != "NULL") 1075 | { 1076 | savedCameraNames.Add(camString); 1077 | } 1078 | // Otherwise stop. 1079 | else 1080 | { 1081 | EndFindKvp(findHandle); 1082 | break; 1083 | } 1084 | } 1085 | var camerasList = new Dictionary(); 1086 | // Loop through all save names (keys) from the list above, convert the string into a dictionary 1087 | // and add it to the dictionary above, with the camera save name as the key. 1088 | foreach (var saveName in savedCameraNames) 1089 | { 1090 | camerasList.Add(saveName, JsonConvert.DeserializeObject(GetResourceKvpString(saveName))); 1091 | } 1092 | // Return the camera dictionary containing all camera save names (keys) linked to the correct camera 1093 | return camerasList; 1094 | } 1095 | 1096 | private async void LoadCameras() 1097 | { 1098 | var savedCameras = GetSavedCameras(); 1099 | scMenuItems = new Dictionary>(); 1100 | 1101 | foreach (var sc in savedCameras) 1102 | { 1103 | MenuItem savedCameraBtn; 1104 | if (sc.Key.Length > 4) 1105 | { 1106 | savedCameraBtn = new MenuItem(sc.Key.Substring(4), $"Manage this saved camera.") 1107 | { 1108 | Label = $"→→→" 1109 | }; 1110 | } 1111 | else 1112 | { 1113 | savedCameraBtn = new MenuItem("NULL", $"Manage this saved camera.") 1114 | { 1115 | Label = $"→→→" 1116 | }; 1117 | } 1118 | savedCamerasMenu.AddMenuItem(savedCameraBtn); 1119 | scMenuItems.Add(savedCameraBtn, sc); 1120 | } 1121 | await Delay(0); 1122 | } 1123 | 1124 | #endregion 1125 | 1126 | #region presets 1127 | 1128 | private static class CustomCamPresets 1129 | { 1130 | public static CameraInfo tandemCam1 = new CameraInfo 1131 | { 1132 | angCamInterpolation_ = 0.02f, 1133 | angCamModifier_ = 0.275f, 1134 | posInterpolation_ = 1f, 1135 | chaseCamMaxAngle_ = 75f, 1136 | linearPosOffset_ = false, 1137 | lockOffsetPos_ = false, 1138 | customCamFOV_ = 60.0f, 1139 | customCamForwardOffset_ = -5.6f, 1140 | customCamUpOffset_ = 2.65f, 1141 | customCamSideOffset_ = 0.0f, 1142 | cameraRollInterpolation_ = 0.045f, 1143 | cameraPitchInterpolation_ = 0.045f, 1144 | pedLock_ = true 1145 | }; 1146 | public static CameraInfo fpvCam1 = new CameraInfo 1147 | { 1148 | angCamInterpolation_ = 0.01f, 1149 | angCamModifier_ = 0.350f, 1150 | posInterpolation_ = 1f, 1151 | chaseCamMaxAngle_ = 75f, 1152 | linearPosOffset_ = false, 1153 | lockOffsetPos_ = true, 1154 | customCamFOV_ = 70.0f, 1155 | customCamForwardOffset_ = -0.2f, 1156 | customCamUpOffset_ = 0.6f, 1157 | customCamSideOffset_ = 0.35f, 1158 | cameraRollInterpolation_ = 0.195f, 1159 | cameraPitchInterpolation_ = 0.5f, 1160 | pedLock_ = false 1161 | }; 1162 | public static CameraInfo NFSCam = new CameraInfo 1163 | { 1164 | angCamInterpolation_ = 0.02f, 1165 | angCamModifier_ = 0.250f, 1166 | posInterpolation_ = 1f, 1167 | chaseCamMaxAngle_ = 75f, 1168 | linearPosOffset_ = true, 1169 | lockOffsetPos_ = false, 1170 | customCamFOV_ = 70.0f, 1171 | customCamForwardOffset_ = -4.05f, 1172 | customCamUpOffset_ = 1.35f, 1173 | customCamSideOffset_ = 0.0f, 1174 | cameraRollInterpolation_ = 0.05f, 1175 | cameraPitchInterpolation_ = 1.0f, 1176 | pedLock_ = true 1177 | }; 1178 | }; 1179 | 1180 | #endregion 1181 | 1182 | private static string _t(string key) { 1183 | return Language.get(key); 1184 | } 1185 | } 1186 | } 1187 | --------------------------------------------------------------------------------