├── .editorconfig ├── .gitignore ├── Directory.Build.props ├── LICENSE ├── OpenGta2.sln ├── OpenGta2.sln.DotSettings ├── gtadocs.md └── src ├── OpenGta2.Client ├── .config │ └── dotnet-tools.json ├── Assets │ ├── AssetManager.cs │ ├── Assets.mgcb │ ├── Effects │ │ ├── BlockFaceEffect.cs │ │ ├── BlockFaceEffect.fx │ │ ├── DebugLineEffect.cs │ │ ├── DebugLineEffect.fx │ │ ├── ScreenspaceSpriteEffect.cs │ │ ├── ScreenspaceSpriteEffect.fx │ │ └── WorldSpriteEffect.cs │ └── Fonts │ │ └── DebugFont.spritefont ├── Camera.cs ├── CameraMode.cs ├── CollisionMap.cs ├── Components │ ├── AudioTestComponent.cs │ ├── BaseComponent.cs │ ├── BaseDrawableComponent.cs │ ├── CameraComponent.cs │ ├── IntroComponent.cs │ ├── MapComponent.cs │ ├── PedManagerComponent.cs │ ├── PlayerControllerComponent.cs │ └── SpriteTestComponent.cs ├── Control.cs ├── Controls.cs ├── Data │ └── BufferArray.cs ├── Diagnostics │ ├── DebuggingDrawingComponent.cs │ ├── DiagnosticHighlight.cs │ ├── DiagnosticValues.cs │ ├── PerformanceCounter.cs │ └── PerformanceCounters.cs ├── GtaGame.cs ├── Icon.ico ├── IntVector2.cs ├── IntVector3.cs ├── Levels │ ├── Face.cs │ ├── GtaVector.cs │ ├── LevelProvider.cs │ ├── RenderableMapChunk.cs │ ├── SlopeGenerator.cs │ └── StyleTextureSet.cs ├── LineSegment2D.cs ├── OpenGta2.Client.csproj ├── Peds │ ├── Ped.cs │ └── PedManager.cs ├── Program.cs ├── Rendering │ ├── FontRenderer.cs │ ├── Light.cs │ ├── QuadRenderer.cs │ ├── VertexPositionSprite.cs │ └── VertexPositionTile.cs ├── Scenes │ ├── IntroScene.cs │ ├── LoadingWorldScene.cs │ ├── Scene.cs │ └── TestWorldScene.cs ├── TestGamePath.cs ├── Utilities │ ├── ComponentActivator.cs │ ├── GameServiceContainerExtensions.cs │ ├── GameTimeExtensions.cs │ └── ThrowHelper.cs └── app.manifest ├── OpenGta2.DebugConsole ├── CarModel.cs ├── LogScriptRuntime.cs ├── OpenGta2.DebugConsole.csproj ├── Program.cs └── TestGamePath.cs ├── OpenGta2.GameData.UnitTests ├── GtaStringReaderTests.cs ├── MapReaderTests.cs ├── OpenGta2.GameData.UnitTests.csproj ├── RiffFileTestBase.cs ├── ScriptInterpreterTests.cs ├── ScriptParserTests.cs ├── StyleReaderTests.cs └── TestGamePath.cs └── OpenGta2.GameData ├── Audio ├── RawReader.cs ├── SdtReader.cs ├── Sound.cs ├── SoundEntry.cs └── SoundLibrary.cs ├── Game └── Vector3.cs ├── GtaStringReader.cs ├── Map ├── MapReader.cs └── Models │ ├── Ang8.cs │ ├── Arrow.cs │ ├── BlockInfo.cs │ ├── ColorArgb.cs │ ├── ColumnInfo.cs │ ├── CompressedMap.cs │ ├── FaceInfo.cs │ ├── Fixed16.cs │ ├── GroundType.cs │ ├── Junction.cs │ ├── JunctionLink.cs │ ├── JunctionSegment.cs │ ├── Map.cs │ ├── MapLight.cs │ ├── MapObject.cs │ ├── MapZone.cs │ ├── Rotation.cs │ ├── SlopeInfo.cs │ ├── SlopeType.cs │ ├── TileAnimation.cs │ └── ZoneType.cs ├── OpenGta2.Data.csproj.DotSettings ├── OpenGta2.GameData.csproj ├── OpenGta2.GameData.csproj.DotSettings ├── Riff ├── RiffChunk.cs ├── RiffChunkNotFoundException.cs ├── RiffChunkStream.cs ├── RiffReader.cs └── RiffReaderExtensions.cs ├── Scripts ├── CommandParameters │ ├── SpawnCarParameters.cs │ └── SpawnPlayerPedParameters.cs ├── Interpreting │ ├── IScriptRuntime.cs │ ├── ScriptCommand.cs │ ├── ScriptCommandFlags.cs │ ├── ScriptCommandType.cs │ └── ScriptInterpreter.cs └── Parsing │ ├── Script.cs │ ├── ScriptParser.cs │ ├── StringTable.cs │ ├── StringType.cs │ └── StringValue.cs ├── StreamExtensions.cs ├── Style ├── Models │ ├── BgraColor.cs │ ├── FontBase.cs │ ├── Palette.cs │ ├── PaletteBase.cs │ ├── PaletteIndex.cs │ ├── PalettePage.cs │ ├── PhysicalPalette.cs │ ├── Sprite.cs │ ├── SpriteBase.cs │ ├── SpriteEntry.cs │ ├── SpriteKind.cs │ ├── SpritePage.cs │ ├── Style.cs │ ├── Tile.cs │ ├── Tiles.cs │ └── TilesPage.cs └── StyleReader.cs └── ThrowHelper.cs /.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/main/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Ll]og/ 33 | [Ll]ogs/ 34 | 35 | # Visual Studio 2015/2017 cache/options directory 36 | .vs/ 37 | # Uncomment if you have tasks that create the project's static files in wwwroot 38 | #wwwroot/ 39 | 40 | # Visual Studio 2017 auto generated files 41 | Generated\ Files/ 42 | 43 | # MSTest test Results 44 | [Tt]est[Rr]esult*/ 45 | [Bb]uild[Ll]og.* 46 | 47 | # NUnit 48 | *.VisualState.xml 49 | TestResult.xml 50 | nunit-*.xml 51 | 52 | # Build Results of an ATL Project 53 | [Dd]ebugPS/ 54 | [Rr]eleasePS/ 55 | dlldata.c 56 | 57 | # Benchmark Results 58 | BenchmarkDotNet.Artifacts/ 59 | 60 | # .NET Core 61 | project.lock.json 62 | project.fragment.lock.json 63 | artifacts/ 64 | 65 | # ASP.NET Scaffolding 66 | ScaffoldingReadMe.txt 67 | 68 | # StyleCop 69 | StyleCopReport.xml 70 | 71 | # Files built by Visual Studio 72 | *_i.c 73 | *_p.c 74 | *_h.h 75 | *.ilk 76 | *.meta 77 | *.obj 78 | *.iobj 79 | *.pch 80 | *.pdb 81 | *.ipdb 82 | *.pgc 83 | *.pgd 84 | *.rsp 85 | *.sbr 86 | *.tlb 87 | *.tli 88 | *.tlh 89 | *.tmp 90 | *.tmp_proj 91 | *_wpftmp.csproj 92 | *.log 93 | *.tlog 94 | *.vspscc 95 | *.vssscc 96 | .builds 97 | *.pidb 98 | *.svclog 99 | *.scc 100 | 101 | # Chutzpah Test files 102 | _Chutzpah* 103 | 104 | # Visual C++ cache files 105 | ipch/ 106 | *.aps 107 | *.ncb 108 | *.opendb 109 | *.opensdf 110 | *.sdf 111 | *.cachefile 112 | *.VC.db 113 | *.VC.VC.opendb 114 | 115 | # Visual Studio profiler 116 | *.psess 117 | *.vsp 118 | *.vspx 119 | *.sap 120 | 121 | # Visual Studio Trace Files 122 | *.e2e 123 | 124 | # TFS 2012 Local Workspace 125 | $tf/ 126 | 127 | # Guidance Automation Toolkit 128 | *.gpState 129 | 130 | # ReSharper is a .NET coding add-in 131 | _ReSharper*/ 132 | *.[Rr]e[Ss]harper 133 | *.DotSettings.user 134 | 135 | # TeamCity is a build add-in 136 | _TeamCity* 137 | 138 | # DotCover is a Code Coverage Tool 139 | *.dotCover 140 | 141 | # AxoCover is a Code Coverage Tool 142 | .axoCover/* 143 | !.axoCover/settings.json 144 | 145 | # Coverlet is a free, cross platform Code Coverage Tool 146 | coverage*.json 147 | coverage*.xml 148 | coverage*.info 149 | 150 | # Visual Studio code coverage results 151 | *.coverage 152 | *.coveragexml 153 | 154 | # NCrunch 155 | _NCrunch_* 156 | .*crunch*.local.xml 157 | nCrunchTemp_* 158 | 159 | # MightyMoose 160 | *.mm.* 161 | AutoTest.Net/ 162 | 163 | # Web workbench (sass) 164 | .sass-cache/ 165 | 166 | # Installshield output folder 167 | [Ee]xpress/ 168 | 169 | # DocProject is a documentation generator add-in 170 | DocProject/buildhelp/ 171 | DocProject/Help/*.HxT 172 | DocProject/Help/*.HxC 173 | DocProject/Help/*.hhc 174 | DocProject/Help/*.hhk 175 | DocProject/Help/*.hhp 176 | DocProject/Help/Html2 177 | DocProject/Help/html 178 | 179 | # Click-Once directory 180 | publish/ 181 | 182 | # Publish Web Output 183 | *.[Pp]ublish.xml 184 | *.azurePubxml 185 | # Note: Comment the next line if you want to checkin your web deploy settings, 186 | # but database connection strings (with potential passwords) will be unencrypted 187 | *.pubxml 188 | *.publishproj 189 | 190 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 191 | # checkin your Azure Web App publish settings, but sensitive information contained 192 | # in these scripts will be unencrypted 193 | PublishScripts/ 194 | 195 | # NuGet Packages 196 | *.nupkg 197 | # NuGet Symbol Packages 198 | *.snupkg 199 | # The packages folder can be ignored because of Package Restore 200 | **/[Pp]ackages/* 201 | # except build/, which is used as an MSBuild target. 202 | !**/[Pp]ackages/build/ 203 | # Uncomment if necessary however generally it will be regenerated when needed 204 | #!**/[Pp]ackages/repositories.config 205 | # NuGet v3's project.json files produces more ignorable files 206 | *.nuget.props 207 | *.nuget.targets 208 | 209 | # Microsoft Azure Build Output 210 | csx/ 211 | *.build.csdef 212 | 213 | # Microsoft Azure Emulator 214 | ecf/ 215 | rcf/ 216 | 217 | # Windows Store app package directories and files 218 | AppPackages/ 219 | BundleArtifacts/ 220 | Package.StoreAssociation.xml 221 | _pkginfo.txt 222 | *.appx 223 | *.appxbundle 224 | *.appxupload 225 | 226 | # Visual Studio cache files 227 | # files ending in .cache can be ignored 228 | *.[Cc]ache 229 | # but keep track of directories ending in .cache 230 | !?*.[Cc]ache/ 231 | 232 | # Others 233 | ClientBin/ 234 | ~$* 235 | *~ 236 | *.dbmdl 237 | *.dbproj.schemaview 238 | *.jfm 239 | *.pfx 240 | *.publishsettings 241 | orleans.codegen.cs 242 | 243 | # Including strong name files can present a security risk 244 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 245 | #*.snk 246 | 247 | # Since there are multiple workflows, uncomment next line to ignore bower_components 248 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 249 | #bower_components/ 250 | 251 | # RIA/Silverlight projects 252 | Generated_Code/ 253 | 254 | # Backup & report files from converting an old project file 255 | # to a newer Visual Studio version. Backup files are not needed, 256 | # because we have git ;-) 257 | _UpgradeReport_Files/ 258 | Backup*/ 259 | UpgradeLog*.XML 260 | UpgradeLog*.htm 261 | ServiceFabricBackup/ 262 | *.rptproj.bak 263 | 264 | # SQL Server files 265 | *.mdf 266 | *.ldf 267 | *.ndf 268 | 269 | # Business Intelligence projects 270 | *.rdl.data 271 | *.bim.layout 272 | *.bim_*.settings 273 | *.rptproj.rsuser 274 | *- [Bb]ackup.rdl 275 | *- [Bb]ackup ([0-9]).rdl 276 | *- [Bb]ackup ([0-9][0-9]).rdl 277 | 278 | # Microsoft Fakes 279 | FakesAssemblies/ 280 | 281 | # GhostDoc plugin setting file 282 | *.GhostDoc.xml 283 | 284 | # Node.js Tools for Visual Studio 285 | .ntvs_analysis.dat 286 | node_modules/ 287 | 288 | # Visual Studio 6 build log 289 | *.plg 290 | 291 | # Visual Studio 6 workspace options file 292 | *.opt 293 | 294 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 295 | *.vbw 296 | 297 | # Visual Studio 6 auto-generated project file (contains which files were open etc.) 298 | *.vbp 299 | 300 | # Visual Studio 6 workspace and project file (working project files containing files to include in project) 301 | *.dsw 302 | *.dsp 303 | 304 | # Visual Studio 6 technical files 305 | *.ncb 306 | *.aps 307 | 308 | # Visual Studio LightSwitch build output 309 | **/*.HTMLClient/GeneratedArtifacts 310 | **/*.DesktopClient/GeneratedArtifacts 311 | **/*.DesktopClient/ModelManifest.xml 312 | **/*.Server/GeneratedArtifacts 313 | **/*.Server/ModelManifest.xml 314 | _Pvt_Extensions 315 | 316 | # Paket dependency manager 317 | .paket/paket.exe 318 | paket-files/ 319 | 320 | # FAKE - F# Make 321 | .fake/ 322 | 323 | # CodeRush personal settings 324 | .cr/personal 325 | 326 | # Python Tools for Visual Studio (PTVS) 327 | __pycache__/ 328 | *.pyc 329 | 330 | # Cake - Uncomment if you are using it 331 | # tools/** 332 | # !tools/packages.config 333 | 334 | # Tabs Studio 335 | *.tss 336 | 337 | # Telerik's JustMock configuration file 338 | *.jmconfig 339 | 340 | # BizTalk build output 341 | *.btp.cs 342 | *.btm.cs 343 | *.odx.cs 344 | *.xsd.cs 345 | 346 | # OpenCover UI analysis results 347 | OpenCover/ 348 | 349 | # Azure Stream Analytics local run output 350 | ASALocalRun/ 351 | 352 | # MSBuild Binary and Structured Log 353 | *.binlog 354 | 355 | # NVidia Nsight GPU debugger configuration file 356 | *.nvuser 357 | 358 | # MFractors (Xamarin productivity tool) working folder 359 | .mfractor/ 360 | 361 | # Local History for Visual Studio 362 | .localhistory/ 363 | 364 | # Visual Studio History (VSHistory) files 365 | .vshistory/ 366 | 367 | # BeatPulse healthcheck temp database 368 | healthchecksdb 369 | 370 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 371 | MigrationBackup/ 372 | 373 | # Ionide (cross platform F# VS Code tools) working folder 374 | .ionide/ 375 | 376 | # Fody - auto-generated XML schema 377 | FodyWeavers.xsd 378 | 379 | # VS Code files for those working on multiple tools 380 | .vscode/* 381 | !.vscode/settings.json 382 | !.vscode/tasks.json 383 | !.vscode/launch.json 384 | !.vscode/extensions.json 385 | *.code-workspace 386 | 387 | # Local History for Visual Studio Code 388 | .history/ 389 | 390 | # Windows Installer files from build outputs 391 | *.cab 392 | *.msi 393 | *.msix 394 | *.msm 395 | *.msp 396 | 397 | # JetBrains Rider 398 | *.sln.iml 399 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | false 5 | true 6 | false 7 | true 8 | AllEnabledByDefault 9 | true 10 | true 11 | latest 12 | 13 | 14 | 15 | 16 | all 17 | runtime; build; native; contentfiles; analyzers 18 | 19 | 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Tim Potze 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. -------------------------------------------------------------------------------- /OpenGta2.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.3.32929.385 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenGta2.GameData", "src\OpenGta2.GameData\OpenGta2.GameData.csproj", "{0ADDBE91-1AB3-41D8-952D-9161A9D08D96}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenGta2.GameData.UnitTests", "src\OpenGta2.GameData.UnitTests\OpenGta2.GameData.UnitTests.csproj", "{D7E24564-8F36-4953-A91A-80D046FE541E}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenGta2.DebugConsole", "src\OpenGta2.DebugConsole\OpenGta2.DebugConsole.csproj", "{3CA4D99C-5FEB-4D1E-86A8-98280A2219A9}" 11 | EndProject 12 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8A2CAF3A-1B0D-4A50-ACD1-7ADCBEA66FD6}" 13 | ProjectSection(SolutionItems) = preProject 14 | .editorconfig = .editorconfig 15 | .gitignore = .gitignore 16 | Directory.Build.props = Directory.Build.props 17 | gtadocs.md = gtadocs.md 18 | EndProjectSection 19 | EndProject 20 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenGta2.Client", "src\OpenGta2.Client\OpenGta2.Client.csproj", "{4743CBAE-F76E-4C54-AE1C-06D84E2128FE}" 21 | EndProject 22 | Global 23 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 24 | Debug|Any CPU = Debug|Any CPU 25 | Release|Any CPU = Release|Any CPU 26 | EndGlobalSection 27 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 28 | {0ADDBE91-1AB3-41D8-952D-9161A9D08D96}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {0ADDBE91-1AB3-41D8-952D-9161A9D08D96}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {0ADDBE91-1AB3-41D8-952D-9161A9D08D96}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {0ADDBE91-1AB3-41D8-952D-9161A9D08D96}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {D7E24564-8F36-4953-A91A-80D046FE541E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {D7E24564-8F36-4953-A91A-80D046FE541E}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {D7E24564-8F36-4953-A91A-80D046FE541E}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {D7E24564-8F36-4953-A91A-80D046FE541E}.Release|Any CPU.Build.0 = Release|Any CPU 36 | {3CA4D99C-5FEB-4D1E-86A8-98280A2219A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 37 | {3CA4D99C-5FEB-4D1E-86A8-98280A2219A9}.Debug|Any CPU.Build.0 = Debug|Any CPU 38 | {3CA4D99C-5FEB-4D1E-86A8-98280A2219A9}.Release|Any CPU.ActiveCfg = Release|Any CPU 39 | {3CA4D99C-5FEB-4D1E-86A8-98280A2219A9}.Release|Any CPU.Build.0 = Release|Any CPU 40 | {4743CBAE-F76E-4C54-AE1C-06D84E2128FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 41 | {4743CBAE-F76E-4C54-AE1C-06D84E2128FE}.Debug|Any CPU.Build.0 = Debug|Any CPU 42 | {4743CBAE-F76E-4C54-AE1C-06D84E2128FE}.Release|Any CPU.ActiveCfg = Release|Any CPU 43 | {4743CBAE-F76E-4C54-AE1C-06D84E2128FE}.Release|Any CPU.Build.0 = Release|Any CPU 44 | EndGlobalSection 45 | GlobalSection(SolutionProperties) = preSolution 46 | HideSolutionNode = FALSE 47 | EndGlobalSection 48 | GlobalSection(ExtensibilityGlobals) = postSolution 49 | SolutionGuid = {406F8BA3-FB34-49C5-B400-B64F78D3DAAE} 50 | EndGlobalSection 51 | EndGlobal 52 | -------------------------------------------------------------------------------- /OpenGta2.sln.DotSettings: -------------------------------------------------------------------------------- 1 |  2 | <Policy Inspect="False" Prefix="" Suffix="" Style="AaBb" /> -------------------------------------------------------------------------------- /gtadocs.md: -------------------------------------------------------------------------------- 1 | ## Data types 2 | - *.sty*: Style graphics file 3 | - *.GMP*: Map file 4 | - *.SEQ*: Map composition file 5 | - *.SCR*: Script file 6 | - *.MMP*: Multiplayer map file 7 | - *.GXT*: Game text file 8 | - *.GCI*: Vehicle parameters file 9 | 10 | ## Resources 11 | - https://gtamp.com/gta2/gta2-gmp-map-file-format/ 12 | - https://gtamp.com/gta2/gta2-style-sty-graphics-file-format/ -------------------------------------------------------------------------------- /src/OpenGta2.Client/.config/dotnet-tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "isRoot": true, 4 | "tools": { 5 | "dotnet-mgcb": { 6 | "version": "3.8.1.303", 7 | "commands": [ 8 | "mgcb" 9 | ] 10 | }, 11 | "dotnet-mgcb-editor": { 12 | "version": "3.8.1.303", 13 | "commands": [ 14 | "mgcb-editor" 15 | ] 16 | }, 17 | "dotnet-mgcb-editor-linux": { 18 | "version": "3.8.1.303", 19 | "commands": [ 20 | "mgcb-editor-linux" 21 | ] 22 | }, 23 | "dotnet-mgcb-editor-windows": { 24 | "version": "3.8.1.303", 25 | "commands": [ 26 | "mgcb-editor-windows" 27 | ] 28 | }, 29 | "dotnet-mgcb-editor-mac": { 30 | "version": "3.8.1.303", 31 | "commands": [ 32 | "mgcb-editor-mac" 33 | ] 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /src/OpenGta2.Client/Assets/AssetManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.Xna.Framework.Content; 3 | using Microsoft.Xna.Framework.Graphics; 4 | using OpenGta2.Client.Assets.Effects; 5 | 6 | namespace OpenGta2.Client.Assets; 7 | 8 | public sealed class AssetManager : IDisposable 9 | { 10 | private BlockFaceEffect? _blockFaceEffect; 11 | private ScreenspaceSpriteEffect? _screenspaceSpriteEffect; 12 | private WorldSpriteEffect? _worldSpriteEffect; 13 | private SpriteFont? _debugFont; 14 | private DebugLineEffect? _debugLineEffect; 15 | 16 | public void LoadContent(ContentManager contentManager) 17 | { 18 | _blockFaceEffect = new BlockFaceEffect(contentManager.Load("Effects/BlockFaceEffect")); 19 | _screenspaceSpriteEffect = new ScreenspaceSpriteEffect(contentManager.Load("Effects/ScreenspaceSpriteEffect")); 20 | _worldSpriteEffect = new WorldSpriteEffect(contentManager.Load("Effects/ScreenspaceSpriteEffect")); 21 | _debugLineEffect = new DebugLineEffect(contentManager.Load("Effects/DebugLineEffect")); 22 | _debugFont = contentManager.Load("Fonts/DebugFont"); 23 | } 24 | 25 | public BlockFaceEffect CreateBlockFaceEffect() 26 | { 27 | return (BlockFaceEffect)_blockFaceEffect!.Clone(); 28 | } 29 | 30 | public ScreenspaceSpriteEffect CreateScreenspaceSpriteEffect() 31 | { 32 | return (ScreenspaceSpriteEffect)_screenspaceSpriteEffect!.Clone(); 33 | } 34 | 35 | public WorldSpriteEffect CreateWorldSpriteEffect() 36 | { 37 | return (WorldSpriteEffect)_worldSpriteEffect!.Clone(); 38 | } 39 | 40 | public DebugLineEffect CreateDebugLineEffect() 41 | { 42 | return (DebugLineEffect)_debugLineEffect!.Clone(); 43 | } 44 | 45 | public SpriteFont GetDebugFont() => _debugFont!; 46 | 47 | public void Dispose() 48 | { 49 | _blockFaceEffect?.Dispose(); 50 | } 51 | } -------------------------------------------------------------------------------- /src/OpenGta2.Client/Assets/Assets.mgcb: -------------------------------------------------------------------------------- 1 | 2 | #----------------------------- Global Properties ----------------------------# 3 | 4 | /outputDir:bin/$(Platform) 5 | /intermediateDir:obj/$(Platform) 6 | /platform:Windows 7 | /config: 8 | /profile:Reach 9 | /compress:False 10 | 11 | #-------------------------------- References --------------------------------# 12 | 13 | 14 | #---------------------------------- Content ---------------------------------# 15 | 16 | #begin Effects/BlockFaceEffect.fx 17 | /importer:EffectImporter 18 | /processor:EffectProcessor 19 | /processorParam:DebugMode=Auto 20 | /build:Effects/BlockFaceEffect.fx 21 | 22 | #begin Effects/DebugLineEffect.fx 23 | /importer:EffectImporter 24 | /processor:EffectProcessor 25 | /processorParam:DebugMode=Auto 26 | /build:Effects/DebugLineEffect.fx 27 | 28 | #begin Effects/ScreenspaceSpriteEffect.fx 29 | /importer:EffectImporter 30 | /processor:EffectProcessor 31 | /processorParam:DebugMode=Auto 32 | /build:Effects/ScreenspaceSpriteEffect.fx 33 | 34 | #begin Fonts/DebugFont.spritefont 35 | /importer:FontDescriptionImporter 36 | /processor:LocalizedFontProcessor 37 | /processorParam:PremultiplyAlpha=True 38 | /processorParam:TextureFormat=Compressed 39 | /build:Fonts/DebugFont.spritefont 40 | 41 | -------------------------------------------------------------------------------- /src/OpenGta2.Client/Assets/Effects/BlockFaceEffect.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.Xna.Framework; 3 | using Microsoft.Xna.Framework.Graphics; 4 | using OpenGta2.Client.Rendering; 5 | 6 | namespace OpenGta2.Client.Assets.Effects; 7 | 8 | public class BlockFaceEffect : Effect, IEffectMatrices 9 | { 10 | public const int MaxLights = 16; 11 | 12 | private readonly EffectParameter _worldViewProjectionParam; 13 | private readonly EffectParameter _worldParam; 14 | private readonly EffectParameter _tilesParam; 15 | private readonly EffectParameter _lightPositionsParam; 16 | private readonly EffectParameter _lightColorsParam; 17 | private readonly EffectParameter _lightRadiiParam; 18 | private readonly EffectParameter _lightIntensitiesParam; 19 | private readonly EffectParameter _lightCountParam; 20 | private readonly EffectParameter _ambientLevelParam; 21 | private readonly EffectParameter _shadingLevelParam; 22 | 23 | private DirtyFlags _dirtyFlags; 24 | 25 | private Matrix _projection; 26 | private Matrix _view; 27 | private Matrix _world; 28 | private Texture2D? _tiles; 29 | private float _ambientLevel = 0.3f; 30 | private float _shadingLevel = 15; 31 | 32 | private readonly Vector3[] _lightPositions = new Vector3[MaxLights]; 33 | private readonly Vector4[] _lightColors = new Vector4[MaxLights]; 34 | private readonly float[] _lightRadii = new float[MaxLights]; 35 | private readonly float[] _lightIntensities = new float[MaxLights]; 36 | private int _lightCount; 37 | 38 | public BlockFaceEffect(Effect cloneSource) : base(cloneSource) 39 | { 40 | _worldViewProjectionParam = Parameters["WorldViewProjection"]; 41 | _worldParam = Parameters["World"]; 42 | _tilesParam = Parameters["Tiles"]; 43 | _lightPositionsParam = Parameters["LightPositions"]; 44 | _lightColorsParam = Parameters["LightColors"]; 45 | _lightRadiiParam = Parameters["LightRadii"]; 46 | _lightIntensitiesParam = Parameters["LightIntensities"]; 47 | _lightCountParam = Parameters["LightCount"]; 48 | _ambientLevelParam = Parameters["AmbientLevel"]; 49 | _shadingLevelParam = Parameters["ShadingLevel"]; 50 | } 51 | 52 | public Matrix Projection 53 | { 54 | get => _projection; 55 | set => Set(ref _projection, value, DirtyFlags.WorldViewProjection); 56 | } 57 | 58 | public Matrix View 59 | { 60 | get => _view; 61 | set => Set(ref _view, value, DirtyFlags.WorldViewProjection); 62 | } 63 | 64 | public Matrix World 65 | { 66 | get => _world; 67 | set => Set(ref _world, value, DirtyFlags.WorldViewProjection | DirtyFlags.World); 68 | } 69 | 70 | public Texture2D? Tiles 71 | { 72 | get => _tiles; 73 | set => Set(ref _tiles, value, DirtyFlags.Tiles); 74 | } 75 | 76 | /// 77 | /// Ambient light level. 0.0 is black, 1.0 is ‘normal’ GTA without light. 0.3 is 'dusk' on Industrial map. 78 | /// 79 | public float AmbientLevel 80 | { 81 | get => _ambientLevel; 82 | set => Set(ref _ambientLevel, value, DirtyFlags.AmbientLevel); 83 | } 84 | 85 | /// 86 | /// Similar to the AmbientLevel, this sets the shading 'contrast' for the level. Valid values are 0 – 31, with 15 being the 'normal' level. 87 | /// 88 | public float ShadingLevel 89 | { 90 | get => _shadingLevel; 91 | set => Set(ref _shadingLevel, value, DirtyFlags.ShadingLevel); 92 | } 93 | 94 | public void SetLights(Span lights) 95 | { 96 | var index = 0; 97 | foreach (var light in lights) 98 | { 99 | _lightPositions[index] = light.Position; 100 | _lightColors[index] = light.Color.ToVector4(); 101 | _lightRadii[index] = light.Radius; 102 | _lightIntensities[index] = light.Intensity; 103 | _dirtyFlags |= DirtyFlags.Lights; 104 | 105 | index++; 106 | if (index == MaxLights) 107 | break; 108 | } 109 | 110 | _lightCount = index; 111 | _dirtyFlags |= DirtyFlags.LightCount; 112 | } 113 | 114 | private void Set(ref T field, T value, DirtyFlags flag) 115 | { 116 | if (field?.Equals(value) ?? false) 117 | return; 118 | 119 | field = value; 120 | _dirtyFlags |= flag; 121 | } 122 | 123 | protected override void OnApply() 124 | { 125 | if ((_dirtyFlags & DirtyFlags.WorldViewProjection) != 0) 126 | _worldViewProjectionParam.SetValue(World * View * Projection); 127 | 128 | if ((_dirtyFlags & DirtyFlags.World) != 0) 129 | _worldParam.SetValue(World); 130 | 131 | if ((_dirtyFlags & DirtyFlags.Tiles) != 0) 132 | _tilesParam.SetValue(_tiles); 133 | 134 | if ((_dirtyFlags & DirtyFlags.Lights) != 0) 135 | { 136 | _lightPositionsParam.SetValue(_lightPositions); 137 | _lightColorsParam.SetValue(_lightColors); 138 | _lightRadiiParam.SetValue(_lightRadii); 139 | _lightIntensitiesParam.SetValue(_lightIntensities); 140 | } 141 | 142 | if ((_dirtyFlags & DirtyFlags.LightCount) != 0) 143 | _lightCountParam.SetValue(_lightCount); 144 | 145 | if ((_dirtyFlags & DirtyFlags.AmbientLevel) != 0) 146 | _ambientLevelParam.SetValue(_ambientLevel); 147 | 148 | if ((_dirtyFlags & DirtyFlags.ShadingLevel) != 0) 149 | _shadingLevelParam.SetValue(_shadingLevel); 150 | 151 | _dirtyFlags = DirtyFlags.None; 152 | 153 | base.OnApply(); 154 | } 155 | 156 | public override Effect Clone() 157 | { 158 | return new BlockFaceEffect(this); 159 | } 160 | 161 | [Flags] 162 | private enum DirtyFlags 163 | { 164 | None = 0, 165 | WorldViewProjection = 1, 166 | Tiles = 2, 167 | Lights = 4, 168 | LightCount = 8, 169 | World = 16, 170 | AmbientLevel = 32, 171 | ShadingLevel = 64 172 | } 173 | } -------------------------------------------------------------------------------- /src/OpenGta2.Client/Assets/Effects/BlockFaceEffect.fx: -------------------------------------------------------------------------------- 1 | #if OPENGL 2 | #define SV_POSITION POSITION 3 | #define VS_SHADERMODEL vs_3_0 4 | #define PS_SHADERMODEL ps_3_0 5 | #else 6 | #define VS_SHADERMODEL vs_4_0 7 | #define PS_SHADERMODEL ps_4_0 8 | #endif 9 | 10 | #define MAX_LIGHTS 16 11 | 12 | matrix World; 13 | matrix WorldViewProjection; 14 | 15 | Texture2DArray Tiles : register(t0); 16 | sampler TilesSampler : register(s0); 17 | 18 | float3 LightPositions[MAX_LIGHTS]; 19 | float4 LightColors[MAX_LIGHTS]; 20 | float LightRadii[MAX_LIGHTS]; 21 | float LightIntensities[MAX_LIGHTS]; 22 | int LightCount = 0; 23 | 24 | float AmbientLevel = 0.3; 25 | float ShadingLevel = 15; 26 | 27 | struct VertexShaderInput 28 | { 29 | float4 Position : POSITION0; 30 | float3 TexCoord : TEXCOORD0; 31 | float Shading : COLOR0; 32 | }; 33 | 34 | struct VertexShaderOutput 35 | { 36 | float4 Position : SV_POSITION; 37 | float3 TexCoord : TEXCOORD0; 38 | float3 WorldPosition : TEXCOORD1; 39 | float Shading : COLOR0; 40 | }; 41 | 42 | float4 CalcPointLight(int index, const in float3 worldPos) 43 | { 44 | const float3 lightDirection = worldPos - LightPositions[index]; 45 | const float distance = length(lightDirection); 46 | 47 | if (distance > LightRadii[index]) 48 | { 49 | return float4(0, 0, 0, 0); 50 | } 51 | 52 | const float intensity = LightIntensities[index]; 53 | const float distanceFactor = 1 - distance / LightRadii[index]; 54 | const float attenuation = intensity * distanceFactor; // linear attenuation 55 | 56 | return LightColors[index] * attenuation; 57 | } 58 | 59 | VertexShaderOutput MainVS(const in VertexShaderInput input) 60 | { 61 | VertexShaderOutput output; 62 | output.Position = mul(input.Position, WorldViewProjection); 63 | output.TexCoord = input.TexCoord; 64 | output.WorldPosition = mul(input.Position, World); 65 | output.Shading = input.Shading; 66 | return output; 67 | } 68 | 69 | float4 MainPS(const in VertexShaderOutput input, bool flatPass) : COLOR 70 | { 71 | float4 color = Tiles.Sample(TilesSampler, input.TexCoord); 72 | 73 | // apply transparency in flat pass 74 | if (flatPass) 75 | { 76 | // clip transparent pixels as not to fill depth buffer 77 | clip(color.a - 0.05); 78 | } 79 | 80 | // apply shading 81 | const float brightness = 1 - input.Shading * (ShadingLevel / 31.0); 82 | color = float4(color.rgb * brightness, color.a); 83 | 84 | // compute lighting 85 | float4 lightTotal = float4(AmbientLevel, AmbientLevel, AmbientLevel, 1); 86 | for (int i = 0; i < LightCount; i++) 87 | { 88 | lightTotal += CalcPointLight(i, input.WorldPosition); 89 | } 90 | 91 | return color * lightTotal; 92 | } 93 | 94 | float4 OpaquePS(const in VertexShaderOutput input) : COLOR 95 | { 96 | return MainPS(input, false); 97 | } 98 | 99 | float4 FlatPS(const in VertexShaderOutput input) : COLOR 100 | { 101 | return MainPS(input, true); 102 | } 103 | 104 | technique Faces 105 | { 106 | pass Opaque 107 | { 108 | VertexShader = compile VS_SHADERMODEL MainVS(); 109 | PixelShader = compile PS_SHADERMODEL OpaquePS(); 110 | } 111 | pass Flat 112 | { 113 | AlphaBlendEnable = TRUE; 114 | VertexShader = compile VS_SHADERMODEL MainVS(); 115 | PixelShader = compile PS_SHADERMODEL FlatPS(); 116 | } 117 | }; -------------------------------------------------------------------------------- /src/OpenGta2.Client/Assets/Effects/DebugLineEffect.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Xna.Framework; 2 | using Microsoft.Xna.Framework.Graphics; 3 | 4 | namespace OpenGta2.Client.Assets.Effects; 5 | 6 | public class DebugLineEffect : Effect 7 | { 8 | private readonly EffectParameter _matrixParam; 9 | 10 | public Matrix TransformMatrix { get; set; } 11 | 12 | public DebugLineEffect(Effect effect) 13 | : base(effect) 14 | { 15 | _matrixParam = Parameters["MatrixTransform"]; 16 | } 17 | 18 | protected override void OnApply() 19 | { 20 | _matrixParam.SetValue(TransformMatrix); 21 | } 22 | 23 | public override Effect Clone() 24 | { 25 | return new DebugLineEffect(this); 26 | } 27 | } -------------------------------------------------------------------------------- /src/OpenGta2.Client/Assets/Effects/DebugLineEffect.fx: -------------------------------------------------------------------------------- 1 | #if OPENGL 2 | #define SV_POSITION POSITION 3 | #define VS_SHADERMODEL vs_3_0 4 | #define PS_SHADERMODEL ps_3_0 5 | #else 6 | #define VS_SHADERMODEL vs_4_0 7 | #define PS_SHADERMODEL ps_4_0 8 | #endif 9 | 10 | matrix MatrixTransform; 11 | 12 | struct VertexShaderInput 13 | { 14 | float4 Position : POSITION0; 15 | float4 Color : COLOR0; 16 | }; 17 | 18 | struct VertexShaderOutput 19 | { 20 | float4 Position : SV_POSITION; 21 | float4 Color : COLOR0; 22 | }; 23 | 24 | VertexShaderOutput MainVS(const in VertexShaderInput input) 25 | { 26 | VertexShaderOutput output; 27 | output.Position = mul(input.Position, MatrixTransform); 28 | output.Color = input.Color; 29 | return output; 30 | } 31 | 32 | float4 MainPS(const in VertexShaderOutput input) : COLOR 33 | { 34 | return input.Color; 35 | } 36 | 37 | technique T0 38 | { 39 | pass P0 40 | { 41 | VertexShader = compile VS_SHADERMODEL MainVS(); 42 | PixelShader = compile PS_SHADERMODEL MainPS(); 43 | } 44 | }; -------------------------------------------------------------------------------- /src/OpenGta2.Client/Assets/Effects/ScreenspaceSpriteEffect.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Xna.Framework; 2 | using Microsoft.Xna.Framework.Graphics; 3 | 4 | namespace OpenGta2.Client.Assets.Effects; 5 | 6 | public class ScreenspaceSpriteEffect : Effect 7 | { 8 | private readonly EffectParameter _matrixParam; 9 | private readonly EffectParameter _textureParam; 10 | 11 | private Viewport _lastViewport; 12 | private Matrix _projection; 13 | 14 | public Texture2D? Texture { get; set; } 15 | public Matrix? TransformMatrix { get; set; } 16 | 17 | public ScreenspaceSpriteEffect(Effect effect) 18 | : base(effect) 19 | { 20 | _matrixParam = Parameters["MatrixTransform"]; 21 | _textureParam = Parameters["Texture"]; 22 | } 23 | 24 | protected override void OnApply() 25 | { 26 | var vp = GraphicsDevice.Viewport; 27 | if (vp.Width != _lastViewport.Width || vp.Height != _lastViewport.Height) 28 | { 29 | Matrix.CreateOrthographicOffCenter(0, vp.Width, 0, vp.Height, -1, 1, out _projection); 30 | 31 | _projection = Matrix.CreateOrthographicOffCenter(0, vp.Width, vp.Height, 0, 0, -10); 32 | 33 | if (GraphicsDevice.UseHalfPixelOffset) 34 | { 35 | _projection.M41 += -0.5f * _projection.M11; 36 | _projection.M42 += -0.5f * _projection.M22; 37 | } 38 | 39 | _lastViewport = vp; 40 | } 41 | 42 | _textureParam?.SetValue(Texture); 43 | 44 | if (TransformMatrix.HasValue) 45 | _matrixParam.SetValue(TransformMatrix.GetValueOrDefault() * _projection); 46 | else 47 | _matrixParam.SetValue(_projection); 48 | } 49 | 50 | public override Effect Clone() 51 | { 52 | return new ScreenspaceSpriteEffect(this); 53 | } 54 | } -------------------------------------------------------------------------------- /src/OpenGta2.Client/Assets/Effects/ScreenspaceSpriteEffect.fx: -------------------------------------------------------------------------------- 1 | #if OPENGL 2 | #define SV_POSITION POSITION 3 | #define VS_SHADERMODEL vs_3_0 4 | #define PS_SHADERMODEL ps_3_0 5 | #else 6 | #define VS_SHADERMODEL vs_4_0 7 | #define PS_SHADERMODEL ps_4_0 8 | #endif 9 | 10 | matrix MatrixTransform; 11 | float4 Color = float4(1, 1, 1, 1); 12 | 13 | Texture2D Texture : register(t0); 14 | sampler TextureSampler : register(s0) = sampler_state 15 | { 16 | Texture = ; 17 | AddressU = clamp; 18 | AddressV = clamp; 19 | }; 20 | 21 | struct VertexShaderInput 22 | { 23 | float4 Position : POSITION0; 24 | float2 TexCoord : TEXCOORD0; 25 | }; 26 | 27 | struct VertexShaderOutput 28 | { 29 | float4 Position : SV_POSITION; 30 | float2 TexCoord : TEXCOORD0; 31 | }; 32 | 33 | VertexShaderOutput MainVS(const in VertexShaderInput input) 34 | { 35 | VertexShaderOutput output; 36 | output.Position = mul(input.Position, MatrixTransform); 37 | output.TexCoord = input.TexCoord; 38 | return output; 39 | } 40 | 41 | float4 MainPS(const in VertexShaderOutput input) : COLOR 42 | { 43 | return Texture.Sample(TextureSampler, input.TexCoord) * Color; 44 | } 45 | 46 | technique T0 47 | { 48 | pass P0 49 | { 50 | VertexShader = compile VS_SHADERMODEL MainVS(); 51 | PixelShader = compile PS_SHADERMODEL MainPS(); 52 | } 53 | }; -------------------------------------------------------------------------------- /src/OpenGta2.Client/Assets/Effects/WorldSpriteEffect.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Xna.Framework; 2 | using Microsoft.Xna.Framework.Graphics; 3 | 4 | namespace OpenGta2.Client.Assets.Effects; 5 | 6 | public class WorldSpriteEffect : Effect 7 | { 8 | private readonly EffectParameter _matrixParam; 9 | private readonly EffectParameter _textureParam; 10 | private readonly EffectParameter _colorParam; 11 | 12 | public Texture2D? Texture { get; set; } 13 | public Matrix TransformMatrix { get; set; } 14 | public Color Color { get; set; } = Color.White; 15 | 16 | public WorldSpriteEffect(Effect effect) 17 | : base(effect) 18 | { 19 | _matrixParam = Parameters["MatrixTransform"]; 20 | _textureParam = Parameters["Texture"]; 21 | _colorParam = Parameters["Color"]; 22 | } 23 | 24 | protected override void OnApply() 25 | { 26 | _textureParam?.SetValue(Texture); 27 | _matrixParam.SetValue(TransformMatrix); 28 | _colorParam.SetValue(Color.ToVector4()); 29 | } 30 | 31 | public override Effect Clone() 32 | { 33 | return new WorldSpriteEffect(this); 34 | } 35 | } -------------------------------------------------------------------------------- /src/OpenGta2.Client/Assets/Fonts/DebugFont.spritefont: -------------------------------------------------------------------------------- 1 |  2 | 8 | 9 | 10 | 11 | 14 | Consolas 15 | 16 | 20 | 15 21 | 22 | 26 | 0 27 | 28 | 32 | true 33 | 34 | 38 | 39 | 40 | 44 | 45 | 46 | 55 | 56 | 57 | 58 | ~ 59 | 60 | 61 | 66 | 67 | 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /src/OpenGta2.Client/Camera.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Xna.Framework; 2 | using OpenGta2.Client.Levels; 3 | using OpenGta2.Client.Peds; 4 | 5 | namespace OpenGta2.Client; 6 | 7 | public class Camera 8 | { 9 | private readonly GameWindow _window; 10 | 11 | public Camera(GameWindow window) 12 | { 13 | _window = window; 14 | } 15 | 16 | public Vector3 Position { get; set; } = new(11.5f, 2.5f, 20); 17 | 18 | public Matrix ViewMatrix => Matrix.CreateLookAt(Position, Position - GtaVector.Skywards, GtaVector.Up); 19 | 20 | public Matrix Projection => GetProjection(); 21 | 22 | public Matrix ProjectionLhs => 23 | Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver4, // 90 fov 24 | _window.ClientBounds.Width / (float)_window.ClientBounds.Height, 0.1f, Position.Z + 1); 25 | 26 | 27 | public BoundingFrustum Frustum { get; } = new(Matrix.Identity); 28 | 29 | public Ped? AttachedToPed { get; private set; } 30 | 31 | public CameraMode Mode { get; private set; } 32 | 33 | public void Attach(Ped ped) 34 | { 35 | AttachedToPed = ped; 36 | Mode = CameraMode.AttachedToPed; 37 | } 38 | 39 | public void Free() 40 | { 41 | Mode = CameraMode.Free; 42 | } 43 | 44 | public void Unfree() 45 | { 46 | if (AttachedToPed != null) 47 | { 48 | Mode = CameraMode.AttachedToPed; 49 | } 50 | } 51 | 52 | private Matrix GetProjection() 53 | { 54 | var p = ProjectionLhs; 55 | 56 | // Invert matrix because DirectX is LHS and MonoGame is RHS 57 | p.M11 = -p.M11; 58 | p.M13 = -p.M13; 59 | 60 | return p; 61 | } 62 | } -------------------------------------------------------------------------------- /src/OpenGta2.Client/CameraMode.cs: -------------------------------------------------------------------------------- 1 | namespace OpenGta2.Client; 2 | 3 | public enum CameraMode 4 | { 5 | Free, 6 | AttachedToPed 7 | } -------------------------------------------------------------------------------- /src/OpenGta2.Client/CollisionMap.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Xna.Framework; 2 | using OpenGta2.Client.Diagnostics; 3 | using OpenGta2.Client.Levels; 4 | using OpenGta2.GameData.Map; 5 | 6 | namespace OpenGta2.Client; 7 | 8 | public class CollisionMap 9 | { 10 | private readonly Map _map; 11 | 12 | public CollisionMap(Map map) 13 | { 14 | _map = map; 15 | } 16 | 17 | public Vector3 CalculateMovement(Vector3 point, float radius, Vector2 delta) 18 | { 19 | var z = (int)point.Z; 20 | 21 | var result = CalculateMovement(new Vector2(point.X, point.Y), z, radius, delta); 22 | 23 | return new Vector3(result, point.Z); 24 | } 25 | 26 | public Vector2 CalculateMovement(Vector2 point, int z, float radius, Vector2 delta) 27 | { 28 | var target = point + delta; 29 | 30 | var min = Vector2.Min(point, target) - new Vector2(radius); 31 | var max = Vector2.Max(point, target) + new Vector2(radius); 32 | 33 | var minInt = IntVector2.Floor(min); 34 | var maxInt = IntVector2.Ceiling(max); 35 | 36 | var wall = GetCollidingWall(minInt, maxInt, z, point, radius, delta); 37 | 38 | if (wall != null) 39 | { 40 | return point; 41 | } 42 | 43 | return target; 44 | } 45 | 46 | private Wall? GetCollidingWall(IntVector2 min, IntVector2 max, int z, Vector2 point, float radius, Vector2 delta) 47 | { 48 | var target = point + delta; 49 | 50 | var movement = new LineSegment2D(point, target); 51 | 52 | var radSq = radius * radius; 53 | 54 | for (var x = min.X; x <= max.X; x++) 55 | { 56 | for (var y = min.Y; y <= max.Y; y++) 57 | { 58 | var cell = new IntVector2(x, y); 59 | 60 | // --c 61 | // | | 62 | // a--b 63 | var a = cell + Vector2.UnitY; 64 | var b = cell + Vector2.One; 65 | var c = cell + Vector2.UnitX; 66 | 67 | // bottom 68 | var line = new LineSegment2D(a, b); 69 | var intersection = LineSegment2D.Intersection(movement, line); 70 | 71 | var intersectDelta = intersection - point; 72 | 73 | if (Vector2.Dot(delta, intersectDelta) <= 0) 74 | { 75 | // wall is behind player 76 | 77 | DiagnosticHighlight.Add(new Vector3(intersection, z), GtaVector.Skywards, Color.Red); // intersection 78 | 79 | continue; 80 | } 81 | 82 | DiagnosticHighlight.Add(new Vector3(intersection, z), GtaVector.Skywards, Color.Yellow); // intersection 83 | 84 | var maxDelta = intersectDelta.Length(); 85 | var deltaLen = delta.Length(); 86 | 87 | if (intersection.X >= a.X - radius && intersection.X <= b.X + radius && deltaLen > maxDelta) 88 | { 89 | var block1 = _map.TryGetBlock(x, y, z); 90 | var block2 = _map.TryGetBlock(x, y + 1, z); 91 | 92 | if ((block1?.Bottom.Wall ?? false) || (block2?.Top.Wall ?? false)) 93 | { 94 | // intersect! 95 | DiagnosticHighlight.Add(new IntVector3(cell.X, cell.Y, z), Color.Red); // hit block 96 | return new Wall(cell, false); 97 | } 98 | } 99 | } 100 | } 101 | 102 | return null; 103 | } 104 | 105 | /// 106 | /// if false, bottom 107 | private record struct Wall(IntVector2 Cell, bool Right); 108 | } -------------------------------------------------------------------------------- /src/OpenGta2.Client/Components/AudioTestComponent.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.Xna.Framework; 3 | using Microsoft.Xna.Framework.Audio; 4 | using Microsoft.Xna.Framework.Input; 5 | using OpenGta2.GameData.Audio; 6 | 7 | namespace OpenGta2.Client.Components; 8 | 9 | public class AudioTestComponent : BaseComponent 10 | { 11 | private readonly SoundLibrary _library; 12 | private readonly Controls _controls; 13 | private readonly Random _random = new(); 14 | 15 | public AudioTestComponent(GtaGame game, Controls controls) : base(game) 16 | { 17 | using var sdt = TestGamePath.OpenFile("data/Audio/bil.sdt"); 18 | var r = new SdtReader(sdt); 19 | 20 | var entries = r.Read(); 21 | 22 | using var raw = TestGamePath.OpenFile("data/Audio/bil.raw"); 23 | var rr = new RawReader(raw); 24 | _library = rr.Read(entries); 25 | 26 | _controls = controls; 27 | } 28 | 29 | public override void Update(GameTime gameTime) 30 | { 31 | if (_controls.IsKeyDown(Keys.Tab)) 32 | { 33 | var sfx = _library.GetSound(_random.Next(0, 2) == 0 ? 309 : 310); 34 | var se = SoundEffect.FromStream(sfx.Stream); 35 | se.Play(); 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /src/OpenGta2.Client/Components/BaseComponent.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Xna.Framework; 2 | 3 | namespace OpenGta2.Client.Components; 4 | 5 | public abstract class BaseComponent : GameComponent 6 | { 7 | protected BaseComponent(GtaGame game) : base(game) 8 | { 9 | Game = game; 10 | } 11 | 12 | protected new GtaGame Game { get; } 13 | } -------------------------------------------------------------------------------- /src/OpenGta2.Client/Components/BaseDrawableComponent.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Xna.Framework; 2 | 3 | namespace OpenGta2.Client.Components; 4 | 5 | public abstract class BaseDrawableComponent : DrawableGameComponent 6 | { 7 | protected BaseDrawableComponent(GtaGame game) : base(game) 8 | { 9 | Game = game; 10 | } 11 | 12 | protected new GtaGame Game { get; } 13 | } -------------------------------------------------------------------------------- /src/OpenGta2.Client/Components/CameraComponent.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Xna.Framework; 2 | using Microsoft.Xna.Framework.Input; 3 | using OpenGta2.Client.Levels; 4 | using OpenGta2.Client.Utilities; 5 | 6 | namespace OpenGta2.Client.Components; 7 | 8 | public class CameraComponent : BaseComponent 9 | { 10 | private readonly Controls _controls; 11 | 12 | public CameraComponent(GtaGame game, Camera camera, Controls controls) : base(game) 13 | { 14 | Camera = camera; 15 | _controls = controls; 16 | } 17 | 18 | private Camera Camera { get; } 19 | 20 | public override void Update(GameTime gameTime) 21 | { 22 | var cameraInput = GetCamControlsVec(); 23 | 24 | if (cameraInput != Vector3.Zero) 25 | { 26 | Camera.Free(); 27 | Camera.Position += cameraInput * 3 * gameTime.GetDelta() * (Camera.Position.Z * 0.4f); 28 | } 29 | 30 | if (_controls.IsKeyDown(Keys.NumPad0)) 31 | { 32 | Camera.Unfree(); 33 | } 34 | 35 | switch (Camera.Mode) 36 | { 37 | case CameraMode.AttachedToPed: 38 | Camera.Position = Camera.AttachedToPed!.Position + GtaVector.Skywards * 8; 39 | break; 40 | } 41 | 42 | Camera.Frustum.Matrix = Camera.ViewMatrix * Camera.ProjectionLhs; 43 | } 44 | 45 | private Vector3 GetCamControlsVec() 46 | { 47 | var cameraInput = Vector3.Zero; 48 | 49 | if (_controls.IsKeyPressed(Keys.NumPad6)) 50 | cameraInput += GtaVector.Right; 51 | 52 | if (_controls.IsKeyPressed(Keys.NumPad4)) 53 | cameraInput += GtaVector.Left; 54 | 55 | if (_controls.IsKeyPressed(Keys.NumPad8)) 56 | cameraInput += GtaVector.Up; 57 | 58 | if (_controls.IsKeyPressed(Keys.NumPad2)) 59 | cameraInput += GtaVector.Down; 60 | 61 | if (_controls.IsKeyPressed(Keys.NumPad7)) 62 | cameraInput += GtaVector.Skywards; 63 | 64 | if (_controls.IsKeyPressed(Keys.NumPad9)) 65 | cameraInput -= GtaVector.Skywards; 66 | 67 | return cameraInput; 68 | } 69 | } -------------------------------------------------------------------------------- /src/OpenGta2.Client/Components/IntroComponent.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Runtime.InteropServices; 4 | using Microsoft.Xna.Framework; 5 | using Microsoft.Xna.Framework.Graphics; 6 | using Microsoft.Xna.Framework.Input; 7 | using OpenGta2.Client.Assets.Effects; 8 | using OpenGta2.Client.Rendering; 9 | using OpenGta2.Client.Scenes; 10 | 11 | namespace OpenGta2.Client.Components; 12 | 13 | public unsafe class IntroComponent : BaseDrawableComponent 14 | { 15 | [UnmanagedFunctionPointer(CallingConvention.StdCall)] 16 | public delegate IntPtr BinkSndSysOpen(uint param); 17 | 18 | private readonly Scene _nextScene; 19 | private Bink* _bink; 20 | private byte[]? _buffer; 21 | private byte[]? _buffer2; 22 | private uint _currentFrame; 23 | private bool _deactivated; 24 | 25 | private ScreenspaceSpriteEffect? _effect; 26 | private bool _failure; 27 | private uint _height; 28 | private uint _lastFrame; 29 | private uint _pitch; 30 | private Texture2D? _texture; 31 | 32 | private uint _width; 33 | 34 | public IntroComponent(GtaGame game, Scene nextScene) : base(game) 35 | { 36 | _nextScene = nextScene; 37 | } 38 | 39 | public override void Initialize() 40 | { 41 | var bikPath = Path.Combine(TestGamePath.Directory.FullName, "data/Movie/intro.bik"); 42 | 43 | if (!File.Exists(bikPath)) 44 | { 45 | _failure = true; 46 | } 47 | else 48 | { 49 | try 50 | { 51 | LoadLibrary(Path.Combine(TestGamePath.Directory.FullName, "binkw32.dll")); 52 | 53 | IntPtr d8; 54 | DirectSoundCreate8(IntPtr.Zero, &d8, IntPtr.Zero); 55 | BinkSetSoundSystem(BinkOpenDirectSound, (uint)d8.ToInt32()); 56 | 57 | _bink = BinkOpen(Path.Combine(TestGamePath.Directory.FullName, "data/Movie/intro.bik"), 0); 58 | BinkSetSoundOnOff(_bink, 1); 59 | _width = _bink->Width; 60 | _height = _bink->Height; 61 | _pitch = _width * 3; 62 | _currentFrame = 0; 63 | _lastFrame = _bink->LastFrameNum; 64 | _buffer = new byte[_pitch * _height]; 65 | _buffer2 = new byte[_width * 4 * _height]; 66 | } 67 | catch 68 | { 69 | _failure = true; 70 | } 71 | } 72 | 73 | base.Initialize(); 74 | } 75 | 76 | protected override void LoadContent() 77 | { 78 | if (_failure) 79 | { 80 | return; 81 | } 82 | 83 | _texture = new Texture2D(GraphicsDevice, (int)_width, (int)_height, false, SurfaceFormat.Color); 84 | 85 | _effect = Game.AssetManager.CreateScreenspaceSpriteEffect(); 86 | _effect.Texture = _texture; 87 | } 88 | 89 | protected override void UnloadContent() 90 | { 91 | _effect?.Dispose(); 92 | _texture?.Dispose(); 93 | 94 | base.UnloadContent(); 95 | } 96 | 97 | public override void Update(GameTime gameTime) 98 | { 99 | if (_deactivated) 100 | { 101 | return; 102 | } 103 | 104 | 105 | if (_failure || _currentFrame >= _lastFrame || Keyboard.GetState().GetPressedKeyCount() > 0) 106 | { 107 | Game.ActivateScene(_nextScene); 108 | _deactivated = true; 109 | } 110 | } 111 | 112 | protected override void Dispose(bool disposing) 113 | { 114 | if (_bink != null) 115 | { 116 | BinkClose(_bink); 117 | } 118 | 119 | base.Dispose(disposing); 120 | } 121 | 122 | public override void Draw(GameTime gameTime) 123 | { 124 | if (_bink == null || _failure) 125 | { 126 | return; 127 | } 128 | 129 | try 130 | { 131 | BinkDoFrame(_bink); 132 | 133 | fixed (byte* ptr = _buffer) 134 | { 135 | BinkCopyToBuffer(_bink, (IntPtr)ptr, _pitch, _height, 0, 0, 0x4000000); 136 | } 137 | 138 | for (var i = 0; i < _buffer!.Length / 3; i++) 139 | { 140 | // Add alpha component 141 | _buffer2![i * 4 + 0] = _buffer[i * 3 + 2]; 142 | _buffer2[i * 4 + 1] = _buffer[i * 3 + 1]; 143 | _buffer2[i * 4 + 2] = _buffer[i * 3 + 0]; 144 | _buffer2[i * 4 + 3] = 0xff; 145 | } 146 | 147 | _texture!.SetData(_buffer2); 148 | 149 | _effect!.CurrentTechnique.Passes[0].Apply(); 150 | 151 | var vp = GraphicsDevice.Viewport; 152 | QuadRenderer.Render(GraphicsDevice, Vector2.Zero, new Vector2(vp.Width, vp.Height)); 153 | 154 | if (_currentFrame >= _lastFrame) 155 | { 156 | return; 157 | } 158 | 159 | if (BinkWait(_bink) == 0) 160 | { 161 | BinkNextFrame(_bink); 162 | _currentFrame++; 163 | } 164 | } 165 | catch 166 | { 167 | _failure = true; 168 | } 169 | } 170 | 171 | [DllImport("Kernel32.dll")] 172 | private static extern IntPtr LoadLibrary(string path); 173 | 174 | [DllImport("binkw32.dll")] 175 | private static extern Bink* BinkOpen(string name, uint flags); 176 | 177 | [DllImport("binkw32.dll")] 178 | private static extern void BinkClose(Bink* bink); 179 | 180 | [DllImport("binkw32.dll")] 181 | private static extern int BinkCopyToBuffer(Bink* bink, IntPtr dest, uint destPitch, uint destHeight, uint destX, uint destY, uint flags); 182 | 183 | [DllImport("binkw32.dll")] 184 | private static extern int BinkDoFrame(Bink* bink); 185 | 186 | [DllImport("binkw32.dll")] 187 | private static extern int BinkWait(Bink* bink); 188 | 189 | [DllImport("binkw32.dll")] 190 | private static extern void BinkNextFrame(Bink* bink); 191 | 192 | [DllImport("binkw32.dll")] 193 | private static extern void BinkSetSoundOnOff(Bink* bink, int onoff); 194 | 195 | [DllImport("binkw32.dll")] 196 | private static extern IntPtr BinkOpenDirectSound(uint param); 197 | 198 | [DllImport("binkw32.dll")] 199 | private static extern void BinkSetSoundSystem([MarshalAs(UnmanagedType.FunctionPtr)] BinkSndSysOpen open, uint param); 200 | 201 | [DllImport("Dsound.dll")] 202 | private static extern int DirectSoundCreate8(IntPtr lpcGuidDevice, IntPtr* ppDS8, IntPtr pUnkOuter); 203 | 204 | [StructLayout(LayoutKind.Sequential)] 205 | public struct Bink 206 | { 207 | public uint Width; 208 | public uint Height; 209 | public uint Frames; 210 | public uint FrameNum; 211 | public uint LastFrameNum; 212 | } 213 | } -------------------------------------------------------------------------------- /src/OpenGta2.Client/Components/MapComponent.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.Xna.Framework; 3 | using Microsoft.Xna.Framework.Graphics; 4 | using Microsoft.Xna.Framework.Input; 5 | using OpenGta2.Client.Assets.Effects; 6 | using OpenGta2.Client.Diagnostics; 7 | using OpenGta2.Client.Levels; 8 | using OpenGta2.Client.Rendering; 9 | 10 | namespace OpenGta2.Client.Components; 11 | 12 | public class MapComponent : BaseDrawableComponent 13 | { 14 | private readonly Camera _camera; 15 | private readonly LevelProvider _levelProvider; 16 | private readonly Controls _controls; 17 | private BlockFaceEffect? _blockFaceEffect; 18 | private bool _noon; 19 | 20 | public MapComponent(GtaGame game, Camera camera, LevelProvider levelProvider, Controls controls) : base(game) 21 | { 22 | _camera = camera; 23 | _levelProvider = levelProvider; 24 | _controls = controls; 25 | } 26 | 27 | protected override void LoadContent() 28 | { 29 | _blockFaceEffect = Game.AssetManager.CreateBlockFaceEffect(); 30 | _blockFaceEffect.Tiles = _levelProvider.Textures!.TilesTexture; 31 | } 32 | 33 | public override void Update(GameTime gameTime) 34 | { 35 | _levelProvider.Update(_camera); 36 | 37 | if (_controls.IsKeyDown(Keys.OemTilde)) 38 | { 39 | _noon = !_noon; 40 | _blockFaceEffect!.AmbientLevel = _noon ? 1 : 0.3f; 41 | } 42 | } 43 | 44 | public override void Draw(GameTime gameTime) 45 | { 46 | Span lights = stackalloc Light[BlockFaceEffect.MaxLights]; 47 | 48 | _blockFaceEffect!.View = _camera.ViewMatrix; 49 | _blockFaceEffect.Projection = _camera.Projection; 50 | 51 | PerformanceCounters.Drawing.StartMeasurement("DrawOpaque"); 52 | foreach (var chunk in _levelProvider.GetRenderableChunks()) 53 | { 54 | if (chunk.OpaquePrimitiveCount == 0) continue; 55 | 56 | ApplyChunk(chunk); 57 | 58 | _blockFaceEffect.SetLights(CollectLights(lights, chunk.ChunkLocation)); 59 | _blockFaceEffect.CurrentTechnique.Passes["Opaque"].Apply(); 60 | Game.GraphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, chunk.OpaquePrimitiveCount); 61 | } 62 | 63 | PerformanceCounters.Drawing.StopMeasurement(); 64 | 65 | PerformanceCounters.Drawing.StartMeasurement("DrawFlat"); 66 | 67 | foreach (var chunk in _levelProvider.GetRenderableChunks()) 68 | { 69 | if (chunk.FlatPrimitiveCount == 0) continue; 70 | 71 | ApplyChunk(chunk); 72 | 73 | _blockFaceEffect.SetLights(CollectLights(lights, chunk.ChunkLocation)); 74 | _blockFaceEffect.CurrentTechnique.Passes["Flat"].Apply(); 75 | Game.GraphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, chunk.FlatIndexOffset, chunk.FlatPrimitiveCount); 76 | } 77 | 78 | PerformanceCounters.Drawing.StopMeasurement(); 79 | 80 | 81 | base.Draw(gameTime); 82 | } 83 | 84 | private void ApplyChunk(RenderableMapChunk chunk) 85 | { 86 | Game.GraphicsDevice.Indices = chunk.Indices; 87 | Game.GraphicsDevice.SetVertexBuffer(chunk.Vertices); 88 | _blockFaceEffect!.World = chunk.Translation; 89 | } 90 | 91 | 92 | private Span CollectLights(Span buffer, Point chunkLocation) 93 | { 94 | if (_noon) 95 | { 96 | return buffer[..0]; 97 | } 98 | 99 | PerformanceCounters.Drawing.StartMeasurement("CollectLights"); 100 | 101 | // point-light performance isn't that great when rendering many chunks. lets just 102 | // disable point-lights when you zoom out too far. this shouldn't happen in regular play. 103 | var result = _camera.Position.Z > 40 ? buffer[..0] : _levelProvider.CollectLights(buffer, chunkLocation); 104 | 105 | PerformanceCounters.Drawing.StopMeasurement(); 106 | 107 | return result; 108 | } 109 | } -------------------------------------------------------------------------------- /src/OpenGta2.Client/Components/PedManagerComponent.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | using Microsoft.Xna.Framework; 3 | using Microsoft.Xna.Framework.Graphics; 4 | using Microsoft.Xna.Framework.Input; 5 | using OpenGta2.Client.Assets.Effects; 6 | using OpenGta2.Client.Diagnostics; 7 | using OpenGta2.Client.Levels; 8 | using OpenGta2.Client.Peds; 9 | using OpenGta2.Client.Rendering; 10 | using OpenGta2.Client.Utilities; 11 | using OpenGta2.GameData.Style; 12 | 13 | namespace OpenGta2.Client.Components; 14 | 15 | public class PedManagerComponent : BaseDrawableComponent 16 | { 17 | private readonly Controls _controls; 18 | private readonly Camera _camera; 19 | private readonly PedManager _pedManager; 20 | private IndexBuffer? _indices; 21 | private VertexBuffer? _vertices; 22 | private WorldSpriteEffect? _spriteEfect; 23 | private readonly LevelProvider _levelProvider; 24 | 25 | public PedManagerComponent(GtaGame game, Camera camera, PedManager pedManager, Controls controls, LevelProvider levelProvider) : base(game) 26 | { 27 | _controls = controls; 28 | _camera = camera; 29 | _pedManager = pedManager; 30 | _levelProvider = levelProvider; 31 | } 32 | 33 | protected override void LoadContent() 34 | { 35 | _spriteEfect = Game.AssetManager.CreateWorldSpriteEffect(); 36 | 37 | _indices = new IndexBuffer(GraphicsDevice, IndexElementSize.SixteenBits, 6, BufferUsage.WriteOnly); 38 | _vertices = new VertexBuffer(GraphicsDevice, typeof(VertexPositionSprite), 4, BufferUsage.WriteOnly); 39 | _vertices.SetData(new VertexPositionSprite[] 40 | { 41 | new( 42 | // top-left 43 | new Vector3(-0.5f, -0.5f, 0), new Vector2(0, 0)), 44 | new( 45 | // top-right 46 | new Vector3(0.5f, -0.5f, 0), new Vector2(1, 0)), 47 | new( 48 | // bottom-left 49 | new Vector3(-0.5f, 0.5f, 0), new Vector2(0, 1)), 50 | new( 51 | // bottom-right 52 | new Vector3(0.5f, 0.5f, 0), new Vector2(1, 1)) 53 | }); 54 | _indices.SetData(new short[] { 0, 1, 2, 2, 1, 3 }); 55 | 56 | base.LoadContent(); 57 | } 58 | 59 | public override void Update(GameTime gameTime) 60 | { 61 | if (_controls.IsKeyDown(Keys.OemPlus)) 62 | { 63 | _pedAnimNum++; 64 | _pedAnimOveride = true; 65 | } 66 | 67 | if (_controls.IsKeyDown(Keys.OemMinus)) 68 | { 69 | _pedAnimNum--; 70 | _pedAnimOveride = true; 71 | } 72 | 73 | foreach (var ped in _pedManager.Peds) 74 | { 75 | ped.UpdateAnimation(gameTime.GetDelta()); 76 | } 77 | 78 | DiagnosticValues.Set("PedAnim", _pedAnimNum.ToString(CultureInfo.InvariantCulture)); 79 | } 80 | 81 | private bool _pedAnimOveride; 82 | private int _pedAnimNum = 53; 83 | 84 | public override void Draw(GameTime gameTime) 85 | { 86 | GraphicsDevice.SetVertexBuffer(_vertices); 87 | GraphicsDevice.Indices = _indices; 88 | 89 | foreach (var ped in _pedManager.Peds) 90 | { 91 | if (!_pedAnimOveride) 92 | { 93 | _pedAnimNum = ped.AnimationFrame + ped.AnimationBase; 94 | } 95 | 96 | _spriteEfect!.Texture = _levelProvider.Textures.GetSpriteTexture(SpriteKind.Ped, (ushort)(158 + _pedAnimNum), ped.Remap); 97 | 98 | var world = Matrix.CreateScale(_spriteEfect!.Texture.Width / 64f, _spriteEfect!.Texture.Height / 64f, 1) * Matrix.CreateRotationZ(ped.Rotation) * Matrix.CreateTranslation(ped.Position); 99 | 100 | // shadow 101 | _spriteEfect.Color = new Color(0, 0, 0, 0.6f); 102 | _spriteEfect.TransformMatrix = world * Matrix.CreateTranslation(new Vector3(4f/64, 4f/64, 0.1f)) * _camera.ViewMatrix * _camera.Projection; 103 | _spriteEfect.CurrentTechnique.Passes[0].Apply(); 104 | 105 | GraphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, 2); 106 | 107 | // colors 108 | _spriteEfect.Color = Color.White; 109 | _spriteEfect.TransformMatrix = world * Matrix.CreateTranslation(new Vector3(0, 0, 0.2f)) * _camera.ViewMatrix * _camera.Projection; 110 | _spriteEfect.CurrentTechnique.Passes[0].Apply(); 111 | 112 | GraphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, 2); 113 | } 114 | 115 | 116 | base.Draw(gameTime); 117 | } 118 | } -------------------------------------------------------------------------------- /src/OpenGta2.Client/Components/PlayerControllerComponent.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.Xna.Framework; 3 | using OpenGta2.Client.Diagnostics; 4 | using OpenGta2.Client.Levels; 5 | using OpenGta2.Client.Peds; 6 | using OpenGta2.Client.Utilities; 7 | 8 | namespace OpenGta2.Client.Components; 9 | 10 | public class PlayerControllerComponent : BaseComponent 11 | { 12 | private readonly Controls _controls; 13 | private readonly PedManager _pedManager; 14 | private readonly LevelProvider _levelProvider; 15 | private readonly Camera _camera; 16 | private Ped? _player; 17 | public PlayerControllerComponent(GtaGame game, Controls controls, PedManager pedManager, LevelProvider levelProvider, Camera camera) : base(game) 18 | { 19 | _controls = controls; 20 | _pedManager = pedManager; 21 | _levelProvider = levelProvider; 22 | _camera = camera; 23 | } 24 | 25 | public override void Initialize() 26 | { 27 | _player = new Ped(new Vector3(11.5f, 2.5f, _levelProvider.Map.GetGroundZ(12, 2)), 0, 25); 28 | _pedManager.Peds.Add(_player); 29 | _camera.Attach(_player); 30 | } 31 | 32 | public override void Update(GameTime gameTime) 33 | { 34 | if (_player == null) return; 35 | 36 | var fd = 0f; 37 | var lr = 0f; 38 | 39 | if (_controls.IsKeyPressed(Control.Right)) 40 | lr++; 41 | 42 | if (_controls.IsKeyPressed(Control.Left)) 43 | lr--; 44 | 45 | if (_controls.IsKeyPressed(Control.Forward)) 46 | fd++; 47 | 48 | if (_controls.IsKeyPressed(Control.Backward)) 49 | fd--; 50 | 51 | _player.Rotation += lr * MathHelper.TwoPi * gameTime.GetDelta(); 52 | 53 | var heading = new Vector2(MathF.Sin(-_player.Rotation), MathF.Cos(-_player.Rotation)); 54 | var delta = heading * fd * gameTime.GetDelta() * 2; 55 | 56 | const float playerWidth = (18f / 64); 57 | 58 | DiagnosticHighlight.Add(_player.Position - new Vector3(playerWidth/2, playerWidth/2, 0), new Vector3(playerWidth, playerWidth, 1), Color.Blue); 59 | 60 | // _player.Position += new Vector3(delta, 0); 61 | _player.Position = _levelProvider.CollisionMap.CalculateMovement(_player.Position, playerWidth, delta); 62 | 63 | _player.Animation = fd != 0 ? PedAnimation.Walking : PedAnimation.Idle; 64 | } 65 | } -------------------------------------------------------------------------------- /src/OpenGta2.Client/Components/SpriteTestComponent.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Xna.Framework; 2 | using OpenGta2.Client.Diagnostics; 3 | using OpenGta2.Client.Levels; 4 | using OpenGta2.Client.Rendering; 5 | 6 | namespace OpenGta2.Client.Components; 7 | 8 | public class SpriteTestComponent : BaseDrawableComponent 9 | { 10 | private FontRenderer? _fontRenderer; 11 | 12 | public SpriteTestComponent(GtaGame game) : base(game) 13 | { 14 | } 15 | 16 | protected override void LoadContent() 17 | { 18 | _fontRenderer = new FontRenderer(Game.AssetManager, Game.Services.GetService()); 19 | } 20 | 21 | public override void Draw(GameTime gameTime) 22 | { 23 | PerformanceCounters.Drawing.StartMeasurement("DrawSpriteTest"); 24 | 25 | // font 26 | _fontRenderer!.Draw(GraphicsDevice, new Vector2(5, 100), 0, "HI"); 27 | 28 | PerformanceCounters.Drawing.StopMeasurement(); 29 | } 30 | } -------------------------------------------------------------------------------- /src/OpenGta2.Client/Control.cs: -------------------------------------------------------------------------------- 1 | namespace OpenGta2.Client; 2 | 3 | public enum Control 4 | { 5 | Forward, 6 | Backward, 7 | Left, 8 | Right, 9 | Shoot, 10 | Menu 11 | } -------------------------------------------------------------------------------- /src/OpenGta2.Client/Controls.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.Xna.Framework.Input; 3 | 4 | namespace OpenGta2.Client; 5 | 6 | public class Controls 7 | { 8 | private KeyboardState _current; 9 | private KeyboardState _previous; 10 | 11 | /// 12 | /// Is key down since current frame. 13 | /// 14 | public bool IsKeyDown(Keys key) 15 | { 16 | return _current.IsKeyDown(key) && _previous.IsKeyUp(key); 17 | } 18 | 19 | /// 20 | /// Is key down since current frame. 21 | /// 22 | public bool IsKeyDown(Control key) 23 | { 24 | return IsKeyDown(GetKey(key)); 25 | } 26 | 27 | /// 28 | /// Is key up since current frame. 29 | /// 30 | public bool IsKeyUp(Keys key) 31 | { 32 | return _current.IsKeyUp(key) && _previous.IsKeyDown(key); 33 | } 34 | 35 | /// 36 | /// Is key up since current frame. 37 | /// 38 | public bool IsKeyUp(Control key) 39 | { 40 | return IsKeyUp(GetKey(key)); 41 | } 42 | 43 | /// 44 | /// Is key down. 45 | /// 46 | public bool IsKeyPressed(Keys key) 47 | { 48 | return _current.IsKeyDown(key); 49 | } 50 | 51 | /// 52 | /// Is key down. 53 | /// 54 | public bool IsKeyPressed(Control key) 55 | { 56 | return IsKeyPressed(GetKey(key)); 57 | } 58 | 59 | private static Keys GetKey(Control control) 60 | { 61 | // should be configurable at some point 62 | return control switch 63 | { 64 | Control.Forward => Keys.Up, 65 | Control.Backward => Keys.Down, 66 | Control.Left => Keys.Left, 67 | Control.Right => Keys.Right, 68 | Control.Shoot => Keys.LeftControl, 69 | Control.Menu => Keys.Escape, 70 | _ => throw new ArgumentOutOfRangeException(nameof(control), control, null) 71 | }; 72 | } 73 | 74 | public void Update() 75 | { 76 | _previous = _current; 77 | _current = Keyboard.GetState(); 78 | } 79 | } -------------------------------------------------------------------------------- /src/OpenGta2.Client/Data/BufferArray.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace OpenGta2.Client.Data; 4 | 5 | public class BufferArray 6 | { 7 | private T[] _buffer = new T[16]; 8 | 9 | private int _length; 10 | 11 | public int Length => _length; 12 | 13 | public void Reset(bool clear = false) 14 | { 15 | _length = 0; 16 | if (clear) 17 | Array.Clear(_buffer); 18 | } 19 | 20 | private void Resize() 21 | { 22 | var buffer = new T[_buffer.Length * 2]; 23 | Array.Copy(_buffer, 0, buffer, 0, _length); 24 | _buffer = buffer; 25 | } 26 | 27 | public void Add(T value) 28 | { 29 | if (_buffer.Length == _length) 30 | Resize(); 31 | 32 | _buffer[_length++] = value; 33 | } 34 | 35 | public T[] GetArray() 36 | { 37 | return _buffer; 38 | } 39 | 40 | public Span AsSpan() 41 | { 42 | return _buffer.AsSpan(0, _length); 43 | } 44 | } -------------------------------------------------------------------------------- /src/OpenGta2.Client/Diagnostics/DebuggingDrawingComponent.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | using System.Text; 3 | using Microsoft.Xna.Framework.Graphics; 4 | using Microsoft.Xna.Framework; 5 | using Microsoft.Xna.Framework.Input; 6 | using OpenGta2.Client.Assets.Effects; 7 | using OpenGta2.Client.Utilities; 8 | 9 | namespace OpenGta2.Client.Diagnostics; 10 | 11 | public class DebuggingDrawingComponent : DrawableGameComponent 12 | { 13 | private readonly Camera _camera; 14 | private readonly Controls _controls; 15 | private readonly StringBuilder _stringBuilder = new(); 16 | 17 | private readonly SpriteBatch _spriteBatch; 18 | private SpriteFont? _font; 19 | private DebugLineEffect? _debugLineEffect; 20 | private float _time; 21 | 22 | private static readonly VertexPositionColor[] _blockVertices = 23 | { 24 | new(new Vector3(0, 1, 0), Color.White), 25 | new(new Vector3(1, 1, 0), Color.White), 26 | new(new Vector3(0, 0, 0), Color.White), 27 | new(new Vector3(1, 0, 0), Color.White), 28 | new(new Vector3(0, 1, 1), Color.White), 29 | new(new Vector3(1, 1, 1), Color.White), 30 | new(new Vector3(0, 0, 1), Color.White), 31 | new(new Vector3(1, 0, 1), Color.White), 32 | }; 33 | 34 | private static readonly short[] _blockIndices = { 0, 1, 1, 3, 3, 2, 2, 0, 0, 4, 1, 5, 3, 7, 2, 6, 4, 5, 5, 7, 7, 6, 6, 4 }; 35 | 36 | public DebuggingDrawingComponent(Game game, Camera camera, Controls controls) : base(game) 37 | { 38 | _camera = camera; 39 | _controls = controls; 40 | _spriteBatch = new SpriteBatch(GraphicsDevice); 41 | } 42 | 43 | private new GtaGame Game => (GtaGame)base.Game; 44 | 45 | public override void Initialize() 46 | { 47 | DrawOrder = 1000; 48 | 49 | base.Initialize(); 50 | } 51 | 52 | protected override void LoadContent() 53 | { 54 | _font = Game.AssetManager.GetDebugFont(); 55 | _debugLineEffect = Game.AssetManager.CreateDebugLineEffect(); 56 | } 57 | 58 | public override void Draw(GameTime gameTime) => Draw(gameTime.GetDelta()); 59 | 60 | public void Draw(float deltaTime) 61 | { 62 | PerformanceCounters.Drawing.StartMeasurement("Debug"); 63 | 64 | if (_controls.IsKeyPressed(Keys.F1)) 65 | { 66 | DrawDiagnosticHighlights(); 67 | } 68 | else 69 | { 70 | DiagnosticHighlight.Reset(); 71 | } 72 | 73 | DrawDiagnosticText(deltaTime); 74 | 75 | PerformanceCounters.Drawing.StopMeasurement(); 76 | } 77 | 78 | private void DrawDiagnosticText(float deltaTime) 79 | { 80 | // draw fps and performance counters 81 | _time += (deltaTime - _time) / 5; 82 | 83 | _stringBuilder.AppendLine(CultureInfo.InvariantCulture, $"FPS: {(1 / _time):N1}"); 84 | PerformanceCounters.Drawing.AppendText(_stringBuilder); 85 | 86 | foreach (var kv in DiagnosticValues.Values) 87 | { 88 | _stringBuilder.AppendLine(CultureInfo.InvariantCulture, $"{kv.Key}: {kv.Value}"); 89 | } 90 | 91 | var text = _stringBuilder.ToString(); 92 | _stringBuilder.Clear(); 93 | 94 | 95 | _spriteBatch.Begin(); 96 | _spriteBatch.DrawString(_font, text, new Vector2(10, 10), Color.White, 0, Vector2.Zero, Vector2.One, SpriteEffects.None, 0); 97 | _spriteBatch.End(); 98 | 99 | PerformanceCounters.Drawing.Reset(); 100 | 101 | // reset DepthStencil state after drawing 2d 102 | GraphicsDevice.DepthStencilState = DepthStencilState.Default; 103 | } 104 | 105 | private void DrawDiagnosticHighlights() 106 | { 107 | var tmp = GraphicsDevice.DepthStencilState; 108 | GraphicsDevice.DepthStencilState = DepthStencilState.None; 109 | foreach (var value in DiagnosticHighlight.HighlightedBlocks) 110 | { 111 | for (var i = 0; i < 8; i++) 112 | { 113 | _blockVertices[i].Color = value.color; 114 | } 115 | 116 | _debugLineEffect!.TransformMatrix = Matrix.CreateScale(value.scale) * Matrix.CreateTranslation(value.block) * _camera.ViewMatrix * _camera.Projection; 117 | _debugLineEffect.CurrentTechnique.Passes[0].Apply(); 118 | 119 | GraphicsDevice.DrawUserIndexedPrimitives(PrimitiveType.LineList, _blockVertices, 0, 8, _blockIndices, 0, 12); 120 | } 121 | 122 | GraphicsDevice.DepthStencilState = tmp; 123 | 124 | DiagnosticHighlight.Reset(); 125 | } 126 | } -------------------------------------------------------------------------------- /src/OpenGta2.Client/Diagnostics/DiagnosticHighlight.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Diagnostics; 3 | using Microsoft.Xna.Framework; 4 | 5 | namespace OpenGta2.Client.Diagnostics; 6 | 7 | public static class DiagnosticHighlight 8 | { 9 | private static readonly List<(Vector3 block, Vector3 scale, Color color)> _highlightedBlocks = new(); 10 | 11 | public static IReadOnlyList<(Vector3 block, Vector3 scale, Color color)> HighlightedBlocks => _highlightedBlocks; 12 | 13 | [Conditional("DEBUG")] 14 | public static void Add(Vector3 point, Vector3 scale, Color color) 15 | { 16 | _highlightedBlocks.Add((point, scale, color)); 17 | } 18 | 19 | [Conditional("DEBUG")] 20 | public static void Add(IntVector3 point, Color color) 21 | { 22 | Add(point, Vector3.One, color); 23 | } 24 | 25 | public static void Reset() 26 | { 27 | _highlightedBlocks.Clear(); 28 | } 29 | } -------------------------------------------------------------------------------- /src/OpenGta2.Client/Diagnostics/DiagnosticValues.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Diagnostics; 3 | 4 | namespace OpenGta2.Client.Diagnostics; 5 | 6 | public static class DiagnosticValues 7 | { 8 | private static readonly Dictionary _values = new(); 9 | 10 | public static IReadOnlyDictionary Values => _values; 11 | 12 | [Conditional("DEBUG")] 13 | public static void Set(string key, string value) => _values[key] = value; 14 | } -------------------------------------------------------------------------------- /src/OpenGta2.Client/Diagnostics/PerformanceCounter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Globalization; 5 | using System.Text; 6 | 7 | namespace OpenGta2.Client.Diagnostics; 8 | 9 | public class PerformanceCounter 10 | { 11 | private readonly Dictionary _counters = new(); 12 | private readonly Dictionary _measurements = new(); 13 | private readonly Stack<(string, long)> _runningMeasurements = new(16); 14 | private readonly StringBuilder _stringBuilder = new(); 15 | 16 | [Conditional("DEBUG")] 17 | public void Add(string key, int count = 1) 18 | { 19 | _counters.TryGetValue(key, out var value); 20 | _counters[key] = value + count; 21 | } 22 | 23 | [Conditional("DEBUG")] 24 | public void StartMeasurement(string name) 25 | { 26 | _runningMeasurements.Push((name, Stopwatch.GetTimestamp())); 27 | } 28 | 29 | [Conditional("DEBUG")] 30 | public void StopMeasurement() 31 | { 32 | var end = Stopwatch.GetTimestamp(); 33 | 34 | var (name, start) = _runningMeasurements.Pop(); 35 | 36 | _measurements.TryGetValue(name, out var value); 37 | var time = TimeSpan.FromTicks(end - start); 38 | 39 | _measurements[name] = value + time; 40 | } 41 | 42 | public string GetText() 43 | { 44 | AppendText(_stringBuilder); 45 | 46 | var result = _stringBuilder.ToString(); 47 | _stringBuilder.Clear(); 48 | return result; 49 | } 50 | 51 | public void AppendText(StringBuilder stringBuilder) 52 | { 53 | foreach (var (key, value) in _counters) 54 | { 55 | stringBuilder.AppendLine(CultureInfo.InvariantCulture, $"{key}: {value}"); 56 | } 57 | 58 | stringBuilder.AppendLine(); 59 | 60 | foreach (var (key, value) in _measurements) 61 | { 62 | stringBuilder.Append(key); 63 | stringBuilder.Append(": "); 64 | stringBuilder.AppendLine(value.TotalMilliseconds.ToString(CultureInfo.InvariantCulture)); 65 | } 66 | } 67 | 68 | public void Reset() 69 | { 70 | foreach (var key in _counters.Keys) 71 | { 72 | _counters[key] = 0; 73 | } 74 | 75 | foreach (var key in _measurements.Keys) 76 | { 77 | _measurements[key] = TimeSpan.Zero; 78 | } 79 | } 80 | 81 | } -------------------------------------------------------------------------------- /src/OpenGta2.Client/Diagnostics/PerformanceCounters.cs: -------------------------------------------------------------------------------- 1 | namespace OpenGta2.Client.Diagnostics; 2 | 3 | public static class PerformanceCounters 4 | { 5 | public static readonly PerformanceCounter Drawing = new(); 6 | } -------------------------------------------------------------------------------- /src/OpenGta2.Client/GtaGame.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.Xna.Framework; 3 | using Microsoft.Xna.Framework.Graphics; 4 | using OpenGta2.Client.Assets; 5 | using OpenGta2.Client.Scenes; 6 | using OpenGta2.Client.Utilities; 7 | using SharpDX.MediaFoundation; 8 | 9 | namespace OpenGta2.Client; 10 | 11 | public class GtaGame : Game 12 | { 13 | private AssetManager? _assetManager; 14 | private readonly Controls _controls = new(); 15 | private bool _hasReceivedUpdate; 16 | 17 | public GtaGame() 18 | { 19 | Content.RootDirectory = "Assets"; 20 | IsMouseVisible = true; 21 | 22 | var graphics = new GraphicsDeviceManager(this); 23 | graphics.GraphicsProfile = GraphicsProfile.HiDef; 24 | graphics.SynchronizeWithVerticalRetrace = true; 25 | graphics.PreparingDeviceSettings += (sender, args) => 26 | { 27 | graphics.PreferMultiSampling = true; 28 | }; 29 | graphics.ApplyChanges(); 30 | graphics.GraphicsDevice.PresentationParameters.MultiSampleCount = 4; 31 | graphics.PreferredBackBufferWidth = 1920; 32 | graphics.PreferredBackBufferHeight = 1080; 33 | graphics.ApplyChanges(); 34 | } 35 | 36 | public AssetManager AssetManager => _assetManager ?? throw ThrowHelper.GetContentNotLoaded(); 37 | 38 | public Scene? ActiveScene { get; private set; } 39 | 40 | private void FirstUpdate() 41 | { 42 | // If we'd activate in LoadContent, the component won't initialize. 43 | 44 | // ActivateScene(new LoadingWorldScene(this, "data/wil.gmp", "data/wil.sty", new TestWorldScene(this))); // LEVEL 1 45 | // ActivateScene(new LoadingWorldScene(this, "data/lorne2e.gmp", "data/wil.sty", new TestWorldScene(this))); // BONUS 1a 46 | // ActivateScene(new LoadingWorldScene(this, "data/ste.gmp", "data/ste.sty", new TestWorldScene(this))); // LEVEL 2 47 | ActivateScene(new LoadingWorldScene(this, "data/bil.gmp", "data/bil.sty", new TestWorldScene(this))); // LEVEL 3 48 | 49 | // ActivateScene((new IntroScene(this, new LoadingWorldScene(this, "data/bil.gmp", "data/bil.sty", new TestWorldScene(this))))); 50 | } 51 | 52 | protected override void LoadContent() 53 | { 54 | _assetManager = new AssetManager(); 55 | _assetManager.LoadContent(Content); 56 | 57 | Services.AddService(_controls); 58 | Services.AddService(_assetManager); 59 | 60 | base.LoadContent(); 61 | } 62 | 63 | public void ActivateScene(Scene scene) 64 | { 65 | foreach (var component in Components) 66 | { 67 | if (component is IDisposable disposable) disposable.Dispose(); 68 | } 69 | 70 | Components.Clear(); 71 | 72 | Components.Add(scene); 73 | ActiveScene = scene; 74 | } 75 | 76 | protected override void Update(GameTime gameTime) 77 | { 78 | _controls.Update(); 79 | 80 | if (!_hasReceivedUpdate) 81 | { 82 | _hasReceivedUpdate = true; 83 | FirstUpdate(); 84 | } 85 | 86 | if (_controls.IsKeyDown(Control.Menu)) 87 | { 88 | Exit(); 89 | } 90 | 91 | base.Update(gameTime); 92 | } 93 | 94 | protected override void Draw(GameTime gameTime) 95 | { 96 | GraphicsDevice.Clear(Color.Black); 97 | base.Draw(gameTime); 98 | } 99 | } -------------------------------------------------------------------------------- /src/OpenGta2.Client/Icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikkentim/opengta2/3cf65b67e8086a318942eed782b42512940b1d2b/src/OpenGta2.Client/Icon.ico -------------------------------------------------------------------------------- /src/OpenGta2.Client/IntVector2.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.Xna.Framework; 3 | 4 | namespace OpenGta2.Client; 5 | 6 | public struct IntVector2 7 | { 8 | public int X; 9 | public int Y; 10 | 11 | public IntVector2(int x, int y) 12 | { 13 | X = x; 14 | Y = y; 15 | } 16 | 17 | public static IntVector2 Floor(Vector2 vec) 18 | { 19 | return new IntVector2((int)vec.X, (int)vec.Y); 20 | } 21 | 22 | public static IntVector2 Ceiling(Vector2 vec) 23 | { 24 | vec = Vector2.Ceiling(vec); 25 | return new IntVector2((int)vec.X, (int)vec.Y); 26 | } 27 | 28 | public bool Equals(IntVector2 other) 29 | { 30 | return X == other.X && Y == other.Y; 31 | } 32 | 33 | public override bool Equals(object? obj) 34 | { 35 | return obj is IntVector2 other && Equals(other); 36 | } 37 | 38 | public override int GetHashCode() 39 | { 40 | return HashCode.Combine(X, Y); 41 | } 42 | 43 | public static bool operator ==(IntVector2 lhs, IntVector2 rhs) 44 | { 45 | return lhs.Equals(rhs); 46 | } 47 | 48 | public static bool operator !=(IntVector2 lhs, IntVector2 rhs) 49 | { 50 | return !lhs.Equals(rhs); 51 | } 52 | 53 | public static implicit operator Vector2(IntVector2 vec) 54 | { 55 | return new Vector2(vec.X, vec.Y); 56 | } 57 | } -------------------------------------------------------------------------------- /src/OpenGta2.Client/IntVector3.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.Xna.Framework; 3 | 4 | namespace OpenGta2.Client; 5 | 6 | public struct IntVector3 7 | { 8 | public int X; 9 | public int Y; 10 | public int Z; 11 | 12 | public IntVector3(int x, int y, int z) 13 | { 14 | X = x; 15 | Y = y; 16 | Z = z; 17 | } 18 | 19 | public static IntVector3 Floor(Vector3 vec) 20 | { 21 | return new IntVector3((int)vec.X, (int)vec.Y, (int)vec.Z); 22 | } 23 | 24 | public static IntVector3 Ceiling(Vector3 vec) 25 | { 26 | vec = Vector3.Ceiling(vec); 27 | return new IntVector3((int)vec.X, (int)vec.Y, (int)vec.Z); 28 | } 29 | 30 | public bool Equals(IntVector3 other) 31 | { 32 | return X == other.X && Y == other.Y && Z == other.Z; 33 | } 34 | 35 | public override bool Equals(object? obj) 36 | { 37 | return obj is IntVector3 other && Equals(other); 38 | } 39 | 40 | public override int GetHashCode() 41 | { 42 | return HashCode.Combine(X, Y, Z); 43 | } 44 | 45 | public static bool operator ==(IntVector3 lhs, IntVector3 rhs) 46 | { 47 | return lhs.Equals(rhs); 48 | } 49 | 50 | public static bool operator !=(IntVector3 lhs, IntVector3 rhs) 51 | { 52 | return !lhs.Equals(rhs); 53 | } 54 | 55 | public static implicit operator Vector3(IntVector3 vec) 56 | { 57 | return new Vector3(vec.X, vec.Y, vec.Z); 58 | } 59 | } -------------------------------------------------------------------------------- /src/OpenGta2.Client/Levels/Face.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace OpenGta2.Client.Levels; 4 | 5 | [Flags] 6 | public enum Face : byte 7 | { 8 | None = 0, 9 | Top = 1, 10 | Bottom = 2, 11 | Left = 4, 12 | Right = 8, 13 | Lid = 16 14 | } -------------------------------------------------------------------------------- /src/OpenGta2.Client/Levels/GtaVector.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Xna.Framework; 2 | 3 | namespace OpenGta2.Client.Levels; 4 | 5 | public static class GtaVector 6 | { 7 | private static readonly Vector3 _left = -Vector3.UnitX; 8 | private static readonly Vector3 _right = Vector3.UnitX; 9 | private static readonly Vector3 _up = -Vector3.UnitY; 10 | private static readonly Vector3 _down = Vector3.UnitY; 11 | private static readonly Vector3 _sky = Vector3.UnitZ; 12 | 13 | /// 14 | /// (-1, 0, 0) 15 | /// 16 | public static Vector3 Left => _left; 17 | 18 | /// 19 | /// (1, 0, 0) 20 | /// 21 | public static Vector3 Right => _right; 22 | 23 | /// 24 | /// (0, -1, 0) 25 | /// 26 | public static Vector3 Up => _up; 27 | 28 | /// 29 | /// (0, 1, 0) 30 | /// 31 | public static Vector3 Down => _down; 32 | 33 | /// 34 | /// (0, 0, 1) 35 | /// 36 | public static Vector3 Skywards => _sky; 37 | } -------------------------------------------------------------------------------- /src/OpenGta2.Client/Levels/LevelProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Linq; 5 | using Microsoft.Xna.Framework; 6 | using Microsoft.Xna.Framework.Graphics; 7 | using OpenGta2.Client.Data; 8 | using OpenGta2.Client.Rendering; 9 | using OpenGta2.Client.Utilities; 10 | using OpenGta2.GameData.Map; 11 | using OpenGta2.GameData.Riff; 12 | using OpenGta2.GameData.Style; 13 | 14 | namespace OpenGta2.Client.Levels; 15 | 16 | public class LevelProvider 17 | { 18 | public const int ChunkSize = 8; 19 | private readonly Vector3[] _cameraCornersBuffer = new Vector3[8]; 20 | private readonly Dictionary _chunks = new(); 21 | private readonly BufferArray<(float drawOrder, short index)> _flatIndices = new(); 22 | 23 | 24 | private readonly GraphicsDevice _graphicsDevice; 25 | 26 | private readonly BufferArray _indices = new(); 27 | private readonly BufferArray _vertices = new(); 28 | private Rectangle _chunkBounds; 29 | private int _maxChunksX; 30 | private int _maxChunksY; 31 | private Map? _map; 32 | private StyleTextureSet? _textures; 33 | private Style? _style; 34 | private CollisionMap? _collisionMap; 35 | 36 | public LevelProvider(GraphicsDevice graphicsDevice) 37 | { 38 | _graphicsDevice = graphicsDevice; 39 | } 40 | 41 | public Map Map => _map ?? throw ThrowHelper.GetLevelNotLoaded(); 42 | 43 | public CollisionMap CollisionMap => _collisionMap ?? throw ThrowHelper.GetLevelNotLoaded(); 44 | 45 | public Style Style => _style ?? throw ThrowHelper.GetLevelNotLoaded(); 46 | 47 | public StyleTextureSet Textures => _textures ?? throw ThrowHelper.GetLevelNotLoaded(); 48 | 49 | 50 | public bool IsMapLoaded => _map != null && _style != null && _textures != null; 51 | 52 | public void LoadLevel(string mapFile, string styleFile) 53 | { 54 | try 55 | { 56 | using var mapStream = TestGamePath.OpenFile(mapFile); 57 | using var mapRiffReader = new RiffReader(mapStream); 58 | var mapreader = new MapReader(mapRiffReader); 59 | 60 | using var styleStream = TestGamePath.OpenFile(styleFile); 61 | using var styleRiffReader = new RiffReader(styleStream); 62 | var styleReader = new StyleReader(styleRiffReader); 63 | 64 | _map = mapreader.Read(); 65 | _style = styleReader.Read(); 66 | _textures = StyleTextureSet.Create(_style, _graphicsDevice); 67 | 68 | _maxChunksX = (int)Math.Ceiling(Map.Width / (float)ChunkSize); 69 | _maxChunksY = (int)Math.Ceiling(Map.Height / (float)ChunkSize); 70 | 71 | _collisionMap = new CollisionMap(_map); 72 | } 73 | catch 74 | { 75 | UnloadLevel(); 76 | throw; 77 | } 78 | } 79 | 80 | public void UnloadLevel() 81 | { 82 | _collisionMap = null; 83 | _map = null; 84 | _style = null; 85 | _textures = null; 86 | } 87 | 88 | public IEnumerable GetRenderableChunks() 89 | { 90 | return _chunks.Values; 91 | } 92 | 93 | public void Update(Camera camera) 94 | { 95 | if (!IsMapLoaded) 96 | return; 97 | 98 | // find visible chunks 99 | camera.Frustum.GetCorners(_cameraCornersBuffer); 100 | var fovBounds = BoundingBox.CreateFromPoints(_cameraCornersBuffer); 101 | 102 | var minX = (int)MathF.Floor(fovBounds.Min.X); 103 | var maxX = MathF.Ceiling(fovBounds.Max.X); 104 | var minY = (int)MathF.Floor(fovBounds.Min.Y); 105 | var maxY = MathF.Ceiling(fovBounds.Max.Y); 106 | 107 | // align with chunk bounds 108 | var chunkMinX = minX / ChunkSize; 109 | var chunkMaxX = (int)MathF.Ceiling(maxX / ChunkSize); 110 | var chunkMinY = minY / ChunkSize; 111 | var chunkMaxY = (int)MathF.Ceiling(maxY / ChunkSize); 112 | 113 | chunkMinX = Math.Clamp(chunkMinX, 0, _maxChunksX); 114 | chunkMaxX = Math.Clamp(chunkMaxX, 0, _maxChunksX); 115 | chunkMinY = Math.Clamp(chunkMinY, 0, _maxChunksY); 116 | chunkMaxY = Math.Clamp(chunkMaxY, 0, _maxChunksY); 117 | 118 | var chunkBounds = new Rectangle(chunkMinX, chunkMinY, chunkMaxX - chunkMinX, chunkMaxY - chunkMinY); 119 | 120 | // unload invisible chunks 121 | for (var x = _chunkBounds.Left; x < _chunkBounds.Right; x++) 122 | { 123 | for (var y = _chunkBounds.Top; y < _chunkBounds.Bottom; y++) 124 | { 125 | if (chunkBounds.Contains(x, y) || !_chunks.TryGetValue(new Point(x, y), out var chunk)) 126 | continue; 127 | 128 | UnloadChunk(chunk); 129 | } 130 | } 131 | 132 | // load new visible chunks 133 | for (var x = chunkBounds.Left; x < chunkBounds.Right; x++) 134 | { 135 | for (var y = chunkBounds.Top; y < chunkBounds.Bottom; y++) 136 | { 137 | if (_chunks.ContainsKey(new Point(x, y))) 138 | // already loaded 139 | continue; 140 | 141 | LoadChunk(x, y); 142 | } 143 | } 144 | 145 | _chunkBounds = chunkBounds; 146 | } 147 | 148 | private void LoadChunk(int chunkX, int chunkY) 149 | { 150 | var point = new Point(chunkX, chunkY); 151 | 152 | var minX = chunkX * ChunkSize; 153 | var maxX = minX + ChunkSize; 154 | var minY = chunkY * ChunkSize; 155 | var maxY = minY + ChunkSize; 156 | 157 | var map = Map.CompressedMap; 158 | 159 | for (var x = minX; x < maxX; x++) 160 | { 161 | for (var y = minY; y < maxY; y++) 162 | { 163 | var column = Map.GetColumn(x, y); 164 | 165 | for (var z = column.Offset; z < column.Height; z++) 166 | { 167 | var offset = new Vector3(x - minX, y - minY, z); 168 | 169 | var blockNum = column.Blocks[z - column.Offset]; 170 | ref var block = ref map.Blocks[blockNum]; 171 | 172 | SlopeGenerator.Push(ref block, offset, _vertices, _indices, _flatIndices); 173 | } 174 | } 175 | } 176 | 177 | var flats = _flatIndices.GetArray() 178 | .Take(_flatIndices.Length) 179 | .OrderBy(x => x.drawOrder) 180 | .Select(x => x.index) 181 | .ToArray(); 182 | 183 | var vert = new VertexBuffer(_graphicsDevice, typeof(VertexPositionTile), _vertices.Length, BufferUsage.WriteOnly); 184 | var idx = new IndexBuffer(_graphicsDevice, typeof(short), _indices.Length + flats.Length, BufferUsage.WriteOnly); 185 | vert.SetData(_vertices.GetArray(), 0, _vertices.Length); 186 | idx.SetData(_indices.GetArray(), 0, _indices.Length); 187 | 188 | idx.SetData(_indices.Length * 2, flats, 0, flats.Length); 189 | 190 | _chunks[point] = new RenderableMapChunk(point, vert, idx, _indices.Length / 3, _indices.Length, flats.Length / 3, 191 | Matrix.CreateTranslation(chunkX * ChunkSize, chunkY * ChunkSize, 0)); 192 | 193 | _vertices.Reset(); 194 | _indices.Reset(); 195 | _flatIndices.Reset(); 196 | } 197 | 198 | private void UnloadChunk(RenderableMapChunk chunk) 199 | { 200 | chunk.Indices.Dispose(); 201 | chunk.Vertices.Dispose(); 202 | 203 | _chunks.Remove(chunk.ChunkLocation); 204 | } 205 | 206 | public Span CollectLights(Span buffer, Point chunkLocation) 207 | { 208 | var minX = chunkLocation.X * ChunkSize; 209 | var minY = chunkLocation.Y * ChunkSize; 210 | var maxX = minX + ChunkSize; 211 | var maxY = minY + ChunkSize; 212 | 213 | // TODO: Performance is terrible. Lights should be in a quadtree for optimization. 214 | var index = 0; 215 | foreach (var light in Map.Lights) 216 | { 217 | if (index == buffer.Length) 218 | { 219 | Debug.WriteLine($"Hit lights limit {buffer.Length} at chunk {chunkLocation}"); 220 | return buffer; // hit limit of lights for this chunk 221 | } 222 | 223 | if (!IsInRadius(minX, maxX, light.Radius, light.X) || !IsInRadius(minY, maxY, light.Radius, light.Y)) 224 | continue; 225 | 226 | var point = new Vector3(light.X, light.Y, light.Z); 227 | var color = new Color(light.ARGB.R, light.ARGB.G, light.ARGB.B, light.ARGB.A); 228 | 229 | buffer[index] = new Light(point, color, light.Radius, light.Intensity / 256f); 230 | 231 | index++; 232 | } 233 | 234 | return buffer[..index]; 235 | } 236 | 237 | private static bool IsInRadius(float min, float max, float radius, float value) 238 | { 239 | return min - radius <= value && max + radius >= value; 240 | } 241 | } -------------------------------------------------------------------------------- /src/OpenGta2.Client/Levels/RenderableMapChunk.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Xna.Framework; 2 | using Microsoft.Xna.Framework.Graphics; 3 | 4 | namespace OpenGta2.Client.Levels; 5 | 6 | public record RenderableMapChunk(Point ChunkLocation, VertexBuffer Vertices, IndexBuffer Indices, int OpaquePrimitiveCount, int FlatIndexOffset, int FlatPrimitiveCount, Matrix Translation); -------------------------------------------------------------------------------- /src/OpenGta2.Client/Levels/StyleTextureSet.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Microsoft.Xna.Framework.Graphics; 4 | using OpenGta2.GameData.Style; 5 | 6 | namespace OpenGta2.Client.Levels; 7 | 8 | public class StyleTextureSet 9 | { 10 | private readonly GraphicsDevice _graphicsDevice; 11 | private readonly Style _style; 12 | private readonly uint[] _buffer = new uint[256 * 256]; 13 | private readonly Dictionary<(SpriteKind kind, ushort number, int remap), Texture2D> _sprites = new(); // TODO: Disposing 14 | 15 | private StyleTextureSet(Texture2D tilesTexture, GraphicsDevice graphicsDevice, Style style) 16 | { 17 | _graphicsDevice = graphicsDevice; 18 | _style = style; 19 | TilesTexture = tilesTexture; 20 | } 21 | 22 | public Texture2D TilesTexture { get; } 23 | 24 | public static StyleTextureSet Create(Style style, GraphicsDevice graphicsDevice) 25 | { 26 | return new StyleTextureSet(CreateTilesTexture(style, graphicsDevice), graphicsDevice, style); 27 | } 28 | 29 | 30 | public Texture2D GetSpriteTexture(SpriteKind kind, ushort number, int remap = -1) 31 | { 32 | if (_sprites.TryGetValue((kind, number, remap), out var texture)) 33 | { 34 | return texture; 35 | } 36 | 37 | texture = CreateSpriteTexture(kind, number, remap); 38 | _sprites[(kind, number, remap)] = texture; 39 | 40 | return texture; 41 | } 42 | 43 | private Texture2D CreateSpriteTexture(SpriteKind kind, ushort number, int remap) 44 | { 45 | var sprite = GetSprite(kind, number); 46 | 47 | var virtualPaletteNumber = sprite.Number + _style.PaletteBase.SpriteOffset; 48 | 49 | if (remap >= 0) 50 | { 51 | var remaps = _style.PaletteBase.GetRemap(kind); 52 | 53 | if (remaps <= remap) 54 | { 55 | throw new ArgumentOutOfRangeException(nameof(remap)); 56 | } 57 | 58 | var remapPalette = _style.PaletteBase.GetRemapOffset(kind); 59 | virtualPaletteNumber = remapPalette + remap; 60 | } 61 | 62 | var physicalPaletteNumber = _style.PaletteIndex.PhysPalette[virtualPaletteNumber]; 63 | var palette = _style.PhysicsalPalette.GetPalette(physicalPaletteNumber); 64 | 65 | for (byte y = 0; y < sprite.Height; y++) 66 | { 67 | for (byte x = 0; x < sprite.Width; x++) 68 | { 69 | _buffer[y * sprite.Width + x] = GetPaletteColor(ref palette, sprite[y, x]); 70 | } 71 | } 72 | 73 | var texture = new Texture2D(_graphicsDevice, sprite.Width, sprite.Height, false, SurfaceFormat.Color); 74 | texture.SetData(_buffer, 0, sprite.Width * sprite.Height); 75 | 76 | return texture; 77 | } 78 | 79 | private Sprite GetSprite(SpriteKind kind, ushort number) 80 | { 81 | var spriteBase = _style.SpriteBases.GetOffset(kind); 82 | var entry = _style.SpriteEntries[spriteBase + number]; 83 | var page = _style.SpriteGraphics[entry.PageNumber]; 84 | return page.GetSprite(entry, (ushort)(spriteBase + number)); 85 | } 86 | 87 | private static Texture2D CreateTilesTexture(Style style, GraphicsDevice graphicsDevice) 88 | { 89 | var tileCount = style.Tiles.TileCount; 90 | 91 | 92 | var result = new Texture2D(graphicsDevice, Tile.Width, Tile.Height, false, SurfaceFormat.Color, 93 | tileCount); 94 | 95 | // style can contain up to 992 tiles, each tile is 64x64 pixels. 96 | var tileData = new uint[Tile.Width * Tile.Height]; 97 | 98 | for (ushort tileNumber = 0; tileNumber < tileCount; tileNumber++) 99 | { 100 | // don't need to add a base for virtual palette number - base for tiles is always 0. 101 | var physicalPaletteNumber = style.PaletteIndex.PhysPalette[tileNumber]; 102 | var palette = style.PhysicsalPalette.GetPalette(physicalPaletteNumber); 103 | 104 | var tile = style.Tiles.GetTile(tileNumber); 105 | 106 | for (byte y = 0; y < Tile.Height; y++) 107 | { 108 | for (byte x = 0; x < Tile.Width; x++) 109 | { 110 | tileData[y * Tile.Width + x] = GetPaletteColor(ref palette, tile[y, x]); 111 | } 112 | } 113 | 114 | result.SetData(0, tileNumber, null, tileData, 0, tileData.Length); 115 | } 116 | 117 | return result; 118 | } 119 | 120 | private static uint GetPaletteColor(ref Palette palette, byte colorEntry) 121 | { 122 | return colorEntry == 0 ? 0 : palette.GetColor(colorEntry).Argb; 123 | } 124 | } -------------------------------------------------------------------------------- /src/OpenGta2.Client/LineSegment2D.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.Xna.Framework; 3 | 4 | namespace OpenGta2.Client; 5 | 6 | public readonly struct LineSegment2D 7 | { 8 | public LineSegment2D(Vector2 from, Vector2 to) 9 | { 10 | From = from; 11 | To = to; 12 | } 13 | 14 | public Vector2 From { get; } 15 | public Vector2 To { get; } 16 | 17 | public static Vector2 Intersection(LineSegment2D a, LineSegment2D b) 18 | { 19 | var c = a.GetComponents(); 20 | var d = b.GetComponents(); 21 | 22 | var u = c.Y * d.Z - d.Y * c.Z; 23 | var v = d.X * c.Z - c.X * d.Z; 24 | var w = c.X * d.Y - d.X * c.Y; 25 | 26 | return new Vector2(u / w, v / w); 27 | } 28 | 29 | public float DistanceToLineSquared(Vector2 point) 30 | { 31 | var l2 = (From - To).LengthSquared(); 32 | if (l2 == 0.0f) return (point - From).LengthSquared(); 33 | 34 | var t = MathF.Max(0, MathF.Min(1, Vector2.Dot(point - From, To - From) / l2)); 35 | var projection = From + t * (To - From); 36 | return (point - projection).LengthSquared(); 37 | } 38 | 39 | private Vector3 GetComponents() 40 | { 41 | var a = From.Y - To.Y; 42 | var b = To.X - From.X; 43 | var c = From.X * To.Y - From.Y * To.X; 44 | 45 | return new Vector3(a, b, c); 46 | } 47 | } -------------------------------------------------------------------------------- /src/OpenGta2.Client/OpenGta2.Client.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | WinExe 4 | net6.0-windows 5 | Major 6 | x86 7 | false 8 | false 9 | true 10 | enable 11 | 12 | 13 | 14 | app.manifest 15 | Icon.ico 16 | PerMonitorV2 17 | true 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/OpenGta2.Client/Peds/Ped.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Xna.Framework; 2 | 3 | namespace OpenGta2.Client.Peds; 4 | 5 | public class Ped 6 | { 7 | private PedAnimation _animation; 8 | 9 | public Ped(Vector3 position, float rotation, int remap) 10 | { 11 | Position = position; 12 | Rotation = rotation; 13 | Remap = remap; 14 | } 15 | 16 | public Vector3 Position { get; set; } 17 | public float Rotation { get; set; } 18 | public int Remap { get; } 19 | 20 | public PedAnimation Animation 21 | { 22 | get => _animation; 23 | set 24 | { 25 | if (_animation == value) return; 26 | 27 | _animation = value; 28 | AnimationFrame = 0; 29 | } 30 | } 31 | 32 | public int AnimationFrame { get; private set; } 33 | 34 | private int MaxAnimationFrame => 35 | Animation switch 36 | { 37 | PedAnimation.Walking => 8, 38 | PedAnimation.Idle => 12, 39 | _ => 0, 40 | }; 41 | 42 | public int AnimationBase => 43 | Animation switch 44 | { 45 | PedAnimation.Walking => 8, 46 | PedAnimation.Idle => 53, 47 | _ => 0, 48 | }; 49 | 50 | private float AnimationFrameTime => 51 | Animation switch 52 | { 53 | PedAnimation.Walking => 0.06f, 54 | PedAnimation.Idle => 0.2f, 55 | _ => 0, 56 | }; 57 | 58 | public void UpdateAnimation(float deltaTime) 59 | { 60 | _animationTime += deltaTime; 61 | 62 | var frameTime = AnimationFrameTime; 63 | if (_animationTime > frameTime) 64 | { 65 | _animationTime -= frameTime; 66 | AnimationFrame++; 67 | 68 | if (AnimationFrame >= MaxAnimationFrame) 69 | AnimationFrame = 0; 70 | } 71 | 72 | } 73 | 74 | private float _animationTime; 75 | } 76 | 77 | public enum PedAnimation 78 | { 79 | Idle, 80 | Walking 81 | } -------------------------------------------------------------------------------- /src/OpenGta2.Client/Peds/PedManager.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace OpenGta2.Client.Peds; 4 | 5 | public class PedManager 6 | { 7 | public List Peds { get; } = new(); 8 | } -------------------------------------------------------------------------------- /src/OpenGta2.Client/Program.cs: -------------------------------------------------------------------------------- 1 | using OpenGta2.Client; 2 | 3 | using var game = new GtaGame(); 4 | game.Window.Title = "Open GTA2"; 5 | game.Run(); -------------------------------------------------------------------------------- /src/OpenGta2.Client/Rendering/FontRenderer.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Xna.Framework; 2 | using Microsoft.Xna.Framework.Graphics; 3 | using OpenGta2.Client.Assets; 4 | using OpenGta2.Client.Assets.Effects; 5 | using OpenGta2.Client.Levels; 6 | using OpenGta2.GameData.Style; 7 | 8 | namespace OpenGta2.Client.Rendering; 9 | 10 | public class FontRenderer 11 | { 12 | private readonly LevelProvider _levelProvider; 13 | private readonly ScreenspaceSpriteEffect _screenspaceSpriteEffect; 14 | 15 | public FontRenderer(AssetManager assetManager, LevelProvider levelProvider) 16 | { 17 | _levelProvider = levelProvider; 18 | _screenspaceSpriteEffect = assetManager.CreateScreenspaceSpriteEffect(); 19 | } 20 | 21 | public void Draw(GraphicsDevice graphicsDevice, Vector2 point, int index, string text, int remap = -1) 22 | { 23 | 24 | var fontOffset = _levelProvider.Style.FontBase.GetFontOffset(index); 25 | 26 | var spaceSize = _levelProvider.Textures.GetSpriteTexture(SpriteKind.Font, (ushort)(fontOffset + ('.' - '!')), remap).Width; 27 | 28 | foreach(var c in text) 29 | { 30 | var ch = c; 31 | if (ch == ' ') 32 | { 33 | // TODO: Don't know how space is handled yet. 34 | point.X += spaceSize; 35 | continue; 36 | } 37 | 38 | var charNum = ch - '!'; 39 | 40 | if (charNum < 0) 41 | { 42 | charNum = '?' - '!'; 43 | } 44 | if (ch > '~') 45 | { 46 | // TODO: Haven't figured out the rest of the charset yet 47 | charNum = '?' - '!'; 48 | } 49 | 50 | _screenspaceSpriteEffect.Texture = _levelProvider.Textures.GetSpriteTexture(SpriteKind.Font, (ushort)(fontOffset + charNum)); 51 | _screenspaceSpriteEffect.CurrentTechnique.Passes[0].Apply(); 52 | 53 | QuadRenderer.Render(graphicsDevice, point, point + new Vector2(_screenspaceSpriteEffect.Texture.Width, _screenspaceSpriteEffect.Texture.Height)); 54 | 55 | point.X += _screenspaceSpriteEffect.Texture.Width; 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /src/OpenGta2.Client/Rendering/Light.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Xna.Framework; 2 | 3 | namespace OpenGta2.Client.Rendering; 4 | 5 | public record struct Light(Vector3 Position, Color Color, float Radius, float Intensity); -------------------------------------------------------------------------------- /src/OpenGta2.Client/Rendering/QuadRenderer.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Xna.Framework; 2 | using Microsoft.Xna.Framework.Graphics; 3 | 4 | namespace OpenGta2.Client.Rendering; 5 | 6 | public static class QuadRenderer 7 | { 8 | private static readonly VertexPositionSprite[] _verts ={ 9 | new( 10 | // top-left 11 | new Vector3(0,0,0), 12 | new Vector2(0,0)), 13 | new( 14 | // top-right 15 | new Vector3(0,0,0), 16 | new Vector2(1,0)), 17 | new( 18 | // bottom-left 19 | new Vector3(0,0,0), 20 | new Vector2(0,1)), 21 | new( 22 | // bottom-right 23 | new Vector3(0,0,0), 24 | new Vector2(1,1)) 25 | }; 26 | 27 | private static readonly short[] _ib = { 0, 1, 2, 2, 1, 3 }; 28 | 29 | public static void Render(GraphicsDevice device, Vector2 v1, Vector2 v2) 30 | { 31 | _verts[0].Position.X = v1.X; 32 | _verts[0].Position.Y = v1.Y; 33 | 34 | _verts[1].Position.X = v2.X; 35 | _verts[1].Position.Y = v1.Y; 36 | 37 | _verts[2].Position.X = v1.X; 38 | _verts[2].Position.Y = v2.Y; 39 | 40 | _verts[3].Position.X = v2.X; 41 | _verts[3].Position.Y = v2.Y; 42 | 43 | 44 | device.DrawUserIndexedPrimitives(PrimitiveType.TriangleList, _verts, 0, 4, _ib, 0, 2); 45 | } 46 | } -------------------------------------------------------------------------------- /src/OpenGta2.Client/Rendering/VertexPositionSprite.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Xna.Framework; 2 | using Microsoft.Xna.Framework.Graphics; 3 | 4 | namespace OpenGta2.Client.Rendering; 5 | 6 | public struct VertexPositionSprite : IVertexType 7 | { 8 | private static readonly VertexDeclaration _declaration = new( 9 | new VertexElement(0, VertexElementFormat.Vector3, VertexElementUsage.Position, 0), 10 | new VertexElement(4 * 3, VertexElementFormat.Vector2, VertexElementUsage.TextureCoordinate, 0) 11 | ); 12 | 13 | VertexDeclaration IVertexType.VertexDeclaration => _declaration; 14 | 15 | public Vector3 Position; 16 | public Vector2 Uv; 17 | 18 | public VertexPositionSprite(Vector3 position, Vector2 uv) 19 | { 20 | 21 | Position = position; 22 | Uv = uv; 23 | } 24 | } -------------------------------------------------------------------------------- /src/OpenGta2.Client/Rendering/VertexPositionTile.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | using Microsoft.Xna.Framework; 3 | using Microsoft.Xna.Framework.Graphics; 4 | 5 | namespace OpenGta2.Client.Rendering; 6 | 7 | [StructLayout(LayoutKind.Sequential, Pack = 1)] 8 | public struct VertexPositionTile : IVertexType 9 | { 10 | private static readonly VertexDeclaration _declaration = new( 11 | new VertexElement(0, VertexElementFormat.Vector3, VertexElementUsage.Position, 0), 12 | new VertexElement(4 * 3, VertexElementFormat.Vector3, VertexElementUsage.TextureCoordinate, 0), 13 | new VertexElement(4 * 6, VertexElementFormat.Single, VertexElementUsage.Color, 0) 14 | ); 15 | 16 | VertexDeclaration IVertexType.VertexDeclaration => _declaration; 17 | 18 | public Vector3 Position; 19 | public Vector3 TextureCoordinate; 20 | public float Shading; 21 | 22 | public VertexPositionTile(Vector3 position, Vector3 textureCoordinate, float shading) 23 | { 24 | Position = position; 25 | TextureCoordinate = textureCoordinate; 26 | Shading = shading; 27 | } 28 | } -------------------------------------------------------------------------------- /src/OpenGta2.Client/Scenes/IntroScene.cs: -------------------------------------------------------------------------------- 1 | using OpenGta2.Client.Assets; 2 | using OpenGta2.Client.Components; 3 | 4 | namespace OpenGta2.Client.Scenes 5 | { 6 | public class IntroScene : Scene 7 | { 8 | private readonly Scene _nextScene; 9 | 10 | public IntroScene(GtaGame game, Scene nextScene) : base(game) 11 | { 12 | _nextScene = nextScene; 13 | } 14 | 15 | public override void Initialize() 16 | { 17 | Game.Components.Add(new IntroComponent(Game, _nextScene)); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/OpenGta2.Client/Scenes/LoadingWorldScene.cs: -------------------------------------------------------------------------------- 1 | using OpenGta2.Client.Levels; 2 | 3 | namespace OpenGta2.Client.Scenes; 4 | 5 | public class LoadingWorldScene : Scene 6 | { 7 | private readonly string _map; 8 | private readonly string _style; 9 | private readonly Scene _nextScene; 10 | 11 | public LoadingWorldScene(GtaGame game, string map, string style, Scene nextScene) : base(game) 12 | { 13 | _map = map; 14 | _style = style; 15 | _nextScene = nextScene; 16 | } 17 | 18 | public override void Initialize() 19 | { 20 | var levelProvider = Game.Services.GetService(); 21 | if (levelProvider == null) 22 | { 23 | levelProvider = new LevelProvider(Game.GraphicsDevice); 24 | Game.Services.AddService(levelProvider); 25 | } 26 | 27 | levelProvider.LoadLevel(_map, _style); 28 | 29 | Game.ActivateScene(_nextScene); 30 | } 31 | } -------------------------------------------------------------------------------- /src/OpenGta2.Client/Scenes/Scene.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Xna.Framework; 2 | using OpenGta2.Client.Utilities; 3 | 4 | namespace OpenGta2.Client.Scenes; 5 | 6 | public abstract class Scene : GameComponent 7 | { 8 | protected Scene(GtaGame game) : base(game) 9 | { 10 | Game = game; 11 | } 12 | 13 | public new GtaGame Game { get; } 14 | 15 | public void AddComponent() where T : IGameComponent 16 | { 17 | var component = ComponentActivator.Activate(Game); 18 | Game.Components.Add(component); 19 | } 20 | } -------------------------------------------------------------------------------- /src/OpenGta2.Client/Scenes/TestWorldScene.cs: -------------------------------------------------------------------------------- 1 | using OpenGta2.Client.Components; 2 | using OpenGta2.Client.Diagnostics; 3 | using OpenGta2.Client.Peds; 4 | using OpenGta2.Client.Utilities; 5 | 6 | namespace OpenGta2.Client.Scenes; 7 | 8 | public class TestWorldScene : Scene 9 | { 10 | public TestWorldScene(GtaGame game) : base(game) 11 | { 12 | Camera = new Camera(game.Window); 13 | } 14 | 15 | public Camera Camera { get; } 16 | 17 | public override void Initialize() 18 | { 19 | Game.Services.ReplaceService(Camera); 20 | 21 | Game.Services.ReplaceService(new PedManager()); 22 | 23 | AddComponent(); 24 | AddComponent(); 25 | AddComponent(); 26 | AddComponent(); 27 | AddComponent(); 28 | AddComponent(); 29 | AddComponent(); 30 | } 31 | } -------------------------------------------------------------------------------- /src/OpenGta2.Client/TestGamePath.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | 4 | namespace OpenGta2.Client; 5 | 6 | public static class TestGamePath 7 | { 8 | public static DirectoryInfo Directory => new(Environment.GetEnvironmentVariable("OPENGTA2_PATH", EnvironmentVariableTarget.User)!); 9 | 10 | public static Stream OpenFile(string path) 11 | { 12 | return File.OpenRead(Path.Combine(Directory.FullName, path)); 13 | } 14 | } -------------------------------------------------------------------------------- /src/OpenGta2.Client/Utilities/ComponentActivator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Linq.Expressions; 4 | using Microsoft.Xna.Framework; 5 | 6 | namespace OpenGta2.Client.Utilities; 7 | 8 | public static class ComponentActivator where T : IGameComponent 9 | { 10 | private static readonly Func _activator = CreateFactory(); 11 | 12 | private static Func CreateFactory() 13 | { 14 | var constructors = typeof(T).GetConstructors(); 15 | 16 | if (constructors.Length != 1) 17 | throw new InvalidOperationException("Only components with a single constructor can be activated."); 18 | 19 | var constructor = constructors.Single(); 20 | var parameters = constructor.GetParameters(); 21 | 22 | var gameArg = Expression.Parameter(typeof(GtaGame)); 23 | var arguments = new Expression[parameters.Length]; 24 | 25 | var serviceProvider = Expression.Property(gameArg, nameof(Game.Services)); 26 | var getServiceMethod = typeof(GameServiceContainer).GetMethod(nameof(GameServiceContainer.GetService), Type.EmptyTypes)!; 27 | 28 | for (var i = 0; i < parameters.Length; i++) 29 | { 30 | var parameter = parameters[i]; 31 | 32 | if (parameter.ParameterType == typeof(GtaGame)) 33 | arguments[i] = gameArg; 34 | else if (parameter.ParameterType == typeof(Game)) 35 | { 36 | arguments[i] = Expression.Convert(gameArg, typeof(Game)); 37 | } 38 | else 39 | { 40 | var method = getServiceMethod.MakeGenericMethod(parameter.ParameterType); 41 | arguments[i] = Expression.Call(serviceProvider, method); 42 | } 43 | } 44 | 45 | 46 | var instance = Expression.New(constructor, arguments); 47 | 48 | return Expression.Lambda>(instance, gameArg).Compile(); 49 | } 50 | 51 | public static T Activate(GtaGame game) 52 | { 53 | return _activator(game); 54 | } 55 | } -------------------------------------------------------------------------------- /src/OpenGta2.Client/Utilities/GameServiceContainerExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Xna.Framework; 2 | 3 | namespace OpenGta2.Client.Utilities; 4 | 5 | public static class GameServiceContainerExtensions 6 | { 7 | public static void ReplaceService(this GameServiceContainer container, T service) 8 | { 9 | container.RemoveService(typeof(T)); 10 | container.AddService(service); 11 | } 12 | } -------------------------------------------------------------------------------- /src/OpenGta2.Client/Utilities/GameTimeExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Xna.Framework; 2 | 3 | namespace OpenGta2.Client.Utilities; 4 | 5 | public static class GameTimeExtensions 6 | { 7 | public static float GetDelta(this GameTime gameTime) 8 | { 9 | return (float)gameTime.ElapsedGameTime.TotalSeconds; 10 | } 11 | } -------------------------------------------------------------------------------- /src/OpenGta2.Client/Utilities/ThrowHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace OpenGta2.Client.Utilities 4 | { 5 | internal static class ThrowHelper 6 | { 7 | public static Exception GetContentNotLoaded() => new InvalidOperationException("The content of this component has not yet been loaded."); 8 | public static Exception GetLevelNotLoaded() => new InvalidOperationException("No level is currently loaded."); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/OpenGta2.Client/app.manifest: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/OpenGta2.DebugConsole/CarModel.cs: -------------------------------------------------------------------------------- 1 | namespace OpenGta2.DebugConsole; 2 | 3 | public enum CarModel 4 | { 5 | // we don't need this data. is only used for debugging purposes. 6 | ALFA, 7 | ALLARD, 8 | AMDB4, 9 | APC, 10 | BANKVAN, 11 | BMW, 12 | BOXCAR, 13 | BOXTRUCK, 14 | BUG, 15 | CAR9, 16 | BUCK, 17 | BUS, 18 | COPCAR, 19 | DART, 20 | EDSEL, 21 | CAR15, 22 | FIAT, 23 | FIRETRUK, 24 | GRAHAM, 25 | GT24640, 26 | CAR20, 27 | GTRUCK, 28 | GUNJEEP, 29 | HOTDOG, 30 | HOTDOG_D1, 31 | HOTDOG_D2, 32 | HOTDOG_D3, 33 | ICECREAM, 34 | ISETLIMO, 35 | ISETTA, 36 | JEEP, 37 | JEFFREY, 38 | LIMO, 39 | LIMO2, 40 | MEDICAR, 41 | MERC, 42 | MESSER, 43 | MIURA, 44 | MONSTER, 45 | MORGAN, 46 | MORRIS, 47 | PICKUP, 48 | RTYPE, 49 | CAR43, 50 | SPIDER, 51 | SPRITE, 52 | STRINRAY, 53 | STRATOS, 54 | STRATOSB, 55 | STRIPETB, 56 | STYPE, 57 | STYPECAB, 58 | SWATVAN, 59 | T2000GT, 60 | TANK, 61 | TANKER, 62 | TAXI, 63 | TBIRD, 64 | TOWTRUCK, 65 | TRAIN, 66 | TRAINCAB, 67 | TRAINFB, 68 | TRANCEAM, 69 | TRUKCAB1, 70 | TRUKCAB2, 71 | TRUKCONT, 72 | TRUKTRNS, 73 | TVVAN, 74 | VAN, 75 | VESPA, 76 | VTYPE, 77 | WBTWIN, 78 | WRECK0, 79 | WRECK1, 80 | WRECK2, 81 | WRECK3, 82 | WRECK4, 83 | WRECK5, 84 | WRECK6, 85 | WRECK7, 86 | WRECK8, 87 | WRECK9, 88 | XK120, 89 | ZCX5, 90 | EDSELFBI, 91 | HOTDOG_D4, 92 | KRSNABUS 93 | } -------------------------------------------------------------------------------- /src/OpenGta2.DebugConsole/LogScriptRuntime.cs: -------------------------------------------------------------------------------- 1 | using OpenGta2.DebugConsole; 2 | using OpenGta2.GameData.Scripts.CommandParameters; 3 | using OpenGta2.GameData.Scripts.Interpreting; 4 | 5 | public class LogScriptRuntime : IScriptRuntime 6 | { 7 | public void SpawnCar(ushort ptrIndex, ScriptCommandType type, SpawnCarParameters arguments) 8 | { 9 | Console.WriteLine(FormattableString.Invariant($"{type} auto{ptrIndex} = {arguments.Position:0.00} {arguments.Remap} {arguments.Rotation} {(CarModel)arguments.Model}")); 10 | } 11 | 12 | public void SpawnPlayerPed(ushort ptrIndex, SpawnPlayerPedParameters arguments) 13 | { 14 | Console.WriteLine(FormattableString.Invariant($"PLAYER_PED p{ptrIndex} = {arguments.Position:0.00} {arguments.Remap} {arguments.Rotation} ")); 15 | } 16 | 17 | public void UnknownCommand(ushort ptrIndex, ScriptCommand command) 18 | { 19 | Console.WriteLine($"UNKNOWN COMMAND: {command.Type} ({command.Type:X})"); 20 | } 21 | } -------------------------------------------------------------------------------- /src/OpenGta2.DebugConsole/OpenGta2.DebugConsole.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net6.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/OpenGta2.DebugConsole/Program.cs: -------------------------------------------------------------------------------- 1 | using OpenGta2.GameData.Map; 2 | using OpenGta2.GameData.Riff; 3 | 4 | /* 5 | using var stream = TestGamePath.OpenFile("data/Industrial-2P.scr"); 6 | var script = new ScriptParser().Parse(stream); 7 | new ScriptInterpreter().Run(script, new LogScriptRuntime()); 8 | */ 9 | 10 | using var stream = TestGamePath.OpenFile("data/bil.gmp"); 11 | using var riffReader = new RiffReader(stream); 12 | var mapreader = new MapReader(riffReader); 13 | 14 | var map2 = mapreader.Read(); 15 | 16 | Console.WriteLine(map2); 17 | 18 | 19 | var map = map2.CompressedMap; 20 | var col = map2.GetColumn(4, 1); 21 | 22 | var bNum = col.Blocks.Last(); 23 | var block = map.Blocks[bNum]; 24 | Console.WriteLine(block.Lid.TileGraphic); 25 | Console.WriteLine("test"); 26 | -------------------------------------------------------------------------------- /src/OpenGta2.DebugConsole/TestGamePath.cs: -------------------------------------------------------------------------------- 1 | public static class TestGamePath 2 | { 3 | public static DirectoryInfo Directory => 4 | new(Environment.GetEnvironmentVariable("OPENGTA2_PATH", EnvironmentVariableTarget.User)!); 5 | 6 | public static Stream OpenFile(string path) => File.OpenRead(Path.Combine(Directory.FullName, path)); 7 | } -------------------------------------------------------------------------------- /src/OpenGta2.GameData.UnitTests/GtaStringReaderTests.cs: -------------------------------------------------------------------------------- 1 | using OpenGta2.GameData; 2 | using OpenGta2.GameData.Riff; 3 | using Shouldly; 4 | using Xunit; 5 | 6 | namespace OpenGta2.Data.UnitTests; 7 | 8 | [Trait("Category", "DataTests")] 9 | public class GtaStringReaderTests 10 | { 11 | [Fact] 12 | public void Read_should_succeed() 13 | { 14 | using var stream = TestGamePath.OpenFile("data/e.gxt"); 15 | using var riff = new RiffReader(stream); 16 | 17 | var sut = new GtaStringReader(riff); 18 | 19 | var result = sut.Read(); 20 | 21 | result.Count.ShouldBe(2595); 22 | 23 | result.ShouldContainKeyAndValue("3648", "!mI knew you were up to the job, Comrade!"); 24 | } 25 | } -------------------------------------------------------------------------------- /src/OpenGta2.GameData.UnitTests/MapReaderTests.cs: -------------------------------------------------------------------------------- 1 | using OpenGta2.GameData.Map; 2 | using Shouldly; 3 | using Xunit; 4 | 5 | namespace OpenGta2.Data.UnitTests; 6 | 7 | [Trait("Category", "DataTests")] 8 | public class MapReaderTests : RiffFileTestBase 9 | { 10 | public MapReaderTests() : base("data/bil.gmp", riff => new MapReader(riff)) 11 | { 12 | } 13 | 14 | [Fact] 15 | public void Read_should_read_compressed_map() 16 | { 17 | var result = Sut.Read(); 18 | 19 | result.Width.ShouldBe(256); 20 | result.Height.ShouldBe(256); 21 | 22 | result.CompressedMap.Base[100, 100].ShouldBe(41903); 23 | result.CompressedMap.Columns[41903].Offset.ShouldBe(2); 24 | result.CompressedMap.Columns[41903].Height.ShouldBe(5); 25 | result.CompressedMap.Columns[41903].Blocks.Length.ShouldBe(3); 26 | } 27 | 28 | [Fact] 29 | public void Read_should_read_objects() 30 | { 31 | var result = Sut.Read(); 32 | 33 | result.Objects.Length.ShouldBe(0); 34 | } 35 | 36 | [Fact] 37 | public void Read_should_read_animations() 38 | { 39 | var result = Sut.Read(); 40 | 41 | result.Animations.Length.ShouldBe(16); 42 | result.Animations[1].Base.ShouldBe(243); 43 | result.Animations[1].FrameRate.ShouldBe(1); 44 | result.Animations[1].Repeat.ShouldBe(0); 45 | } 46 | 47 | [Fact] 48 | public void Read_should_read_zones() 49 | { 50 | var result = Sut.Read(); 51 | 52 | result.Zones.Length.ShouldBe(189); 53 | result.Zones[1].Name.ShouldBe("busstop2"); 54 | result.Zones[1].Type.ShouldBe(ZoneType.BusStop); 55 | } 56 | } -------------------------------------------------------------------------------- /src/OpenGta2.GameData.UnitTests/OpenGta2.GameData.UnitTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | 8 | false 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | runtime; build; native; contentfiles; analyzers; buildtransitive 18 | all 19 | 20 | 21 | runtime; build; native; contentfiles; analyzers; buildtransitive 22 | all 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/OpenGta2.GameData.UnitTests/RiffFileTestBase.cs: -------------------------------------------------------------------------------- 1 | using OpenGta2.GameData.Riff; 2 | 3 | namespace OpenGta2.Data.UnitTests; 4 | 5 | public abstract class RiffFileTestBase : IDisposable 6 | { 7 | private readonly Stream _stream; 8 | private readonly RiffReader _riffReader; 9 | 10 | 11 | protected RiffFileTestBase(string path, Func factory) 12 | { 13 | _stream = TestGamePath.OpenFile(path); 14 | _riffReader = new RiffReader(_stream); 15 | 16 | Sut = factory(_riffReader); 17 | } 18 | 19 | protected T Sut { get; } 20 | 21 | public virtual void Dispose() 22 | { 23 | _stream.Dispose(); 24 | _riffReader.Dispose(); 25 | } 26 | } -------------------------------------------------------------------------------- /src/OpenGta2.GameData.UnitTests/ScriptInterpreterTests.cs: -------------------------------------------------------------------------------- 1 | using Moq; 2 | using OpenGta2.GameData.Scripts; 3 | using OpenGta2.GameData.Scripts.CommandParameters; 4 | using OpenGta2.GameData.Scripts.Interpreting; 5 | using Xunit; 6 | 7 | namespace OpenGta2.Data.UnitTests; 8 | 9 | [Trait("Category", "DataTests")] 10 | public class ScriptInterpreterTests 11 | { 12 | [Fact] 13 | public void Run_should_call_runtime_SpawnCar() 14 | { 15 | using var stream = TestGamePath.OpenFile("data/Industrial-2P.scr"); 16 | var script = new ScriptParser().Parse(stream); 17 | 18 | var runtime = new Mock(); 19 | 20 | var sut = new ScriptInterpreter(); 21 | 22 | sut.Run(script, runtime.Object); 23 | 24 | runtime.Verify(x => x.SpawnCar(It.IsAny(), ScriptCommandType.PARKED_CAR_DATA, It.IsAny()), 25 | Times.Exactly(70)); 26 | } 27 | } -------------------------------------------------------------------------------- /src/OpenGta2.GameData.UnitTests/ScriptParserTests.cs: -------------------------------------------------------------------------------- 1 | using OpenGta2.GameData.Scripts; 2 | using Shouldly; 3 | using Xunit; 4 | 5 | namespace OpenGta2.Data.UnitTests; 6 | 7 | [Trait("Category", "DataTests")] 8 | public class ScriptParserTests 9 | { 10 | [Theory] 11 | [InlineData("data/mike1m.SCR")] 12 | [InlineData("data/wil.SCR")] 13 | public void Parse_should_succeed_with_strings(string path) 14 | { 15 | var sut = new ScriptParser(); 16 | 17 | using var stream = TestGamePath.OpenFile(path); 18 | var script = sut.Parse(stream); 19 | 20 | script.Pointers.Length.ShouldBe(6000); 21 | script.ScriptData.Length.ShouldBe(ushort.MaxValue + 1); 22 | script.Strings.Values.Count.ShouldBeGreaterThan(0); 23 | } 24 | 25 | [Theory] 26 | [InlineData("data/downtown-2p.SCR")] 27 | [InlineData("data/MP2-2P.SCR")] 28 | public void Parse_should_succeed_with_no_strings(string path) 29 | { 30 | var sut = new ScriptParser(); 31 | 32 | using var stream = TestGamePath.OpenFile(path); 33 | var script = sut.Parse(stream); 34 | 35 | script.Pointers.Length.ShouldBe(6000); 36 | script.ScriptData.Length.ShouldBe(ushort.MaxValue + 1); 37 | script.Strings.Values.Count.ShouldBe(0); 38 | } 39 | } -------------------------------------------------------------------------------- /src/OpenGta2.GameData.UnitTests/StyleReaderTests.cs: -------------------------------------------------------------------------------- 1 | using OpenGta2.GameData.Style; 2 | using Shouldly; 3 | using Xunit; 4 | 5 | namespace OpenGta2.Data.UnitTests; 6 | 7 | [Trait("Category", "DataTests")] 8 | public class StyleReaderTests : RiffFileTestBase 9 | { 10 | public StyleReaderTests() : base("data/bil.sty", riff => new StyleReader(riff)) 11 | { 12 | } 13 | 14 | [Fact] 15 | public void Read_should_read_tiles() 16 | { 17 | var result = Sut.Read(); 18 | 19 | result.Tiles.TileCount.ShouldBe(992); 20 | } 21 | 22 | [Fact] 23 | public void Read_should_read_palette_base() 24 | { 25 | var result = Sut.Read(); 26 | 27 | result.PaletteBase.Sprite.ShouldBe(2089); 28 | } 29 | 30 | [Fact] 31 | public void Read_should_read_palette_index() 32 | { 33 | var result = Sut.Read(); 34 | 35 | result.PaletteIndex.PhysPalette[500].ShouldBe(15); 36 | result.PaletteIndex.PhysPalette[5].ShouldBe(0); 37 | } 38 | 39 | [Fact] 40 | public void Read_should_read_physical_palette() 41 | { 42 | var result = Sut.Read(); 43 | 44 | result.PhysicsalPalette.GetPalette(0).GetColor(0).Argb.ShouldBe(0u); 45 | result.PhysicsalPalette.GetPalette(0).GetColor(1).Argb.ShouldBe(4279242760u); 46 | } 47 | 48 | } -------------------------------------------------------------------------------- /src/OpenGta2.GameData.UnitTests/TestGamePath.cs: -------------------------------------------------------------------------------- 1 | namespace OpenGta2.Data.UnitTests; 2 | 3 | public static class TestGamePath 4 | { 5 | public static DirectoryInfo Directory => 6 | new(Environment.GetEnvironmentVariable("OPENGTA2_PATH", EnvironmentVariableTarget.User)!); 7 | 8 | public static Stream OpenFile(string path) => File.OpenRead(Path.Combine(Directory.FullName, path)); 9 | } -------------------------------------------------------------------------------- /src/OpenGta2.GameData/Audio/RawReader.cs: -------------------------------------------------------------------------------- 1 | namespace OpenGta2.GameData.Audio; 2 | 3 | public class RawReader 4 | { 5 | private readonly Stream _stream; 6 | 7 | public RawReader(Stream stream) 8 | { 9 | _stream = stream; 10 | } 11 | 12 | public SoundLibrary Read(SoundEntry[] entries) 13 | { 14 | var bytes = new byte[_stream.Length]; 15 | var n = _stream.Read(bytes); 16 | 17 | if (n != bytes.Length) 18 | { 19 | ThrowHelper.ThrowUnexpectedEndOfStream(); 20 | } 21 | 22 | return new SoundLibrary(bytes, entries); 23 | } 24 | } -------------------------------------------------------------------------------- /src/OpenGta2.GameData/Audio/SdtReader.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace OpenGta2.GameData.Audio 4 | { 5 | public class SdtReader 6 | { 7 | private readonly Stream _stream; 8 | 9 | public SdtReader(Stream stream) 10 | { 11 | _stream = stream; 12 | } 13 | 14 | public SoundEntry[] Read() 15 | { 16 | var entries = new SoundEntry[_stream.Length / Marshal.SizeOf()]; 17 | 18 | _stream.ReadExact(entries.AsSpan()); 19 | return entries; 20 | } 21 | 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/OpenGta2.GameData/Audio/Sound.cs: -------------------------------------------------------------------------------- 1 | namespace OpenGta2.GameData.Audio; 2 | 3 | public record struct Sound(Stream Stream, SoundEntry Entry); -------------------------------------------------------------------------------- /src/OpenGta2.GameData/Audio/SoundEntry.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace OpenGta2.GameData.Audio; 4 | 5 | [StructLayout(LayoutKind.Sequential)] 6 | public struct SoundEntry 7 | { 8 | public int Offset; 9 | public int Size; 10 | public int SampleRate; 11 | public int VariationSampleRate; 12 | public int LoopStart; 13 | public int LoopEnd; 14 | } -------------------------------------------------------------------------------- /src/OpenGta2.GameData/Audio/SoundLibrary.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | using System.Text; 3 | 4 | namespace OpenGta2.GameData.Audio; 5 | 6 | public class SoundLibrary 7 | { 8 | private readonly byte[] _data; 9 | private readonly SoundEntry[] _entries; 10 | 11 | private Random _random = new(); 12 | 13 | public SoundLibrary(byte[] data, SoundEntry[] entries) 14 | { 15 | _data = data; 16 | _entries = entries; 17 | } 18 | 19 | public Sound GetSound(int index) 20 | { 21 | var entry = _entries[index]; 22 | 23 | // generate riff stream with 2 blocks: 'fmt ' and 'data' 24 | // TODO: no allocation 25 | var header = new byte[0x2c]; 26 | 27 | var sampleRate = entry.SampleRate; 28 | 29 | if (entry.VariationSampleRate > 0) 30 | { 31 | sampleRate += _random.Next(-entry.VariationSampleRate, entry.VariationSampleRate); 32 | } 33 | 34 | Encoding.ASCII.GetBytes("RIFF", header.AsSpan(0, 4)); 35 | MemoryMarshal.Cast(header.AsSpan(0x04, 4))[0] = entry.Size + 36; 36 | Encoding.ASCII.GetBytes("WAVE", header.AsSpan(0x08, 4)); 37 | Encoding.ASCII.GetBytes("fmt ", header.AsSpan(0x0c, 4)); 38 | MemoryMarshal.Cast(header.AsSpan(0x10, 4))[0] = 16; // SubchunkSize 39 | MemoryMarshal.Cast(header.AsSpan(0x14, 2))[0] = 1; // AudioFormat = PCM 40 | MemoryMarshal.Cast(header.AsSpan(0x16, 2))[0] = 1; // NumChannels = mono 41 | MemoryMarshal.Cast(header.AsSpan(0x18, 4))[0] = sampleRate; // sample rate 42 | MemoryMarshal.Cast(header.AsSpan(0x1c, 4))[0] = sampleRate * 2; // byte rate 43 | MemoryMarshal.Cast(header.AsSpan(0x20, 2))[0] = 2; // block align 44 | MemoryMarshal.Cast(header.AsSpan(0x22, 2))[0] = 16; // bits per sample 45 | Encoding.ASCII.GetBytes("data", header.AsSpan(0x24, 4)); 46 | MemoryMarshal.Cast(header.AsSpan(0x28, 4))[0] = entry.Size; 47 | 48 | return new Sound(new SoundStream(header, new Memory(_data, entry.Offset, entry.Size)), entry); 49 | } 50 | 51 | private class SoundStream : Stream 52 | { 53 | private readonly byte[] _header; 54 | private Memory _data; 55 | 56 | public SoundStream(byte[] header, Memory data) 57 | { 58 | _header = header; 59 | _data = data; 60 | } 61 | 62 | public override void Flush() => throw new InvalidOperationException(); 63 | 64 | public override int Read(byte[] buffer, int offset, int count) 65 | { 66 | var read = 0; 67 | if (Position < _header.Length) 68 | { 69 | var remainingHeader = _header.Length - (int)Position; 70 | 71 | var readHeader = Math.Min(count, remainingHeader); 72 | 73 | Array.Copy(_header, Position, buffer, offset, readHeader); 74 | 75 | read += readHeader; 76 | Position += readHeader; 77 | 78 | offset += readHeader; 79 | count -= readHeader; 80 | } 81 | 82 | if (count > 0) 83 | { 84 | var readData = (int)Math.Min(Length - Position, count); 85 | _data[..readData].Span.CopyTo(buffer.AsSpan(offset)); 86 | 87 | read += readData; 88 | Position += readData; 89 | } 90 | 91 | return read; 92 | } 93 | 94 | public override long Seek(long offset, SeekOrigin origin) 95 | { 96 | switch (origin) 97 | { 98 | case SeekOrigin.Begin: 99 | Position = offset; 100 | break; 101 | case SeekOrigin.Current: 102 | Position += offset; 103 | break; 104 | case SeekOrigin.End: 105 | Position = Length + offset; 106 | break; 107 | default: 108 | throw new ArgumentOutOfRangeException(nameof(origin), origin, null); 109 | } 110 | 111 | if(Position > Length) 112 | Position = Length; 113 | 114 | return Position; 115 | } 116 | 117 | public override void SetLength(long value) => throw new InvalidOperationException(); 118 | 119 | public override void Write(byte[] buffer, int offset, int count) => throw new InvalidOperationException(); 120 | 121 | public override bool CanRead => true; 122 | public override bool CanSeek => true; 123 | public override bool CanWrite => false; 124 | public override long Length => _data.Length + _header.Length; 125 | public override long Position { get; set; } 126 | } 127 | } -------------------------------------------------------------------------------- /src/OpenGta2.GameData/Game/Vector3.cs: -------------------------------------------------------------------------------- 1 | namespace OpenGta2.GameData.Game; 2 | 3 | public struct Vector3 : IFormattable 4 | { 5 | public float X; 6 | public float Y; 7 | public float Z; 8 | 9 | public Vector3(float x, float y, float z) 10 | { 11 | X = x; 12 | Y = y; 13 | Z = z; 14 | } 15 | 16 | public static Vector3 FromInt(int x, int y, int z) => new(x / 16384.0f, y / 16384.0f, z / 16384.0f); 17 | 18 | public override string ToString() => $"({X}, {Y}, {Z})"; 19 | public string ToString(string? format, IFormatProvider? formatProvider) => $"({X.ToString(format, formatProvider)}, {Y.ToString(format, formatProvider)}, {Z.ToString(format, formatProvider)})"; 20 | public string ToString(string? format) => $"({X.ToString(format)}, {Y.ToString(format)}, {Z.ToString(format)})"; 21 | } -------------------------------------------------------------------------------- /src/OpenGta2.GameData/GtaStringReader.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using OpenGta2.GameData.Riff; 3 | 4 | namespace OpenGta2.GameData; 5 | 6 | public class GtaStringReader 7 | { 8 | private const int SupportedVersion = 100; 9 | 10 | private readonly RiffReader _reader; 11 | 12 | public GtaStringReader(RiffReader reader) 13 | { 14 | if (reader.Type[..3] != "GBL" || reader.Version != SupportedVersion) 15 | { 16 | ThrowHelper.ThrowInvalidFileFormat(); 17 | } 18 | 19 | _reader = reader; 20 | } 21 | 22 | public IDictionary Read() 23 | { 24 | var stringBuilder = new StringBuilder(); 25 | var keys = new Dictionary(); 26 | var result = new Dictionary(); 27 | 28 | // read keys 29 | var keysChunk = _reader.GetRequiredChunk("TKEY"); 30 | while (keysChunk.Stream.Position < keysChunk.Stream.Length) 31 | { 32 | var dataOffset = keysChunk.Stream.ReadExactDoubleWord(); 33 | var name = keysChunk.Stream.ReadExactString(8); 34 | keys[name] = dataOffset; 35 | } 36 | 37 | // read data 38 | var dataChunk = _reader.GetRequiredChunk("TDAT"); 39 | foreach (var kv in keys) 40 | { 41 | dataChunk.Stream.Seek(kv.Value, SeekOrigin.Begin); 42 | result[kv.Key] = ReadString(dataChunk.Stream, stringBuilder); 43 | } 44 | 45 | return result; 46 | } 47 | 48 | private static string ReadString(Stream stream, StringBuilder stringBuilder) 49 | { 50 | stringBuilder.Clear(); 51 | 52 | Span buffer = stackalloc byte[1]; 53 | while (true) 54 | { 55 | var character = stream.ReadByte(); 56 | var modifier = stream.ReadByte(); 57 | 58 | if (character == 0 && modifier == 0 || character < 0 || modifier < 0) 59 | { 60 | break; 61 | } 62 | 63 | if (modifier != 0) 64 | { 65 | buffer[0] = (byte)modifier; 66 | stringBuilder.Append(Encoding.UTF8.GetString(buffer)); 67 | } 68 | 69 | buffer[0] = (byte)character; 70 | stringBuilder.Append(Encoding.UTF8.GetString(buffer)); 71 | } 72 | 73 | return stringBuilder.ToString(); 74 | } 75 | } -------------------------------------------------------------------------------- /src/OpenGta2.GameData/Map/MapReader.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | using System.Runtime.InteropServices; 3 | using System.Text; 4 | using OpenGta2.GameData.Riff; 5 | 6 | namespace OpenGta2.GameData.Map 7 | { 8 | public class MapReader 9 | { 10 | private const int SupportedVersion = 500; 11 | 12 | private readonly RiffReader _riffReader; 13 | 14 | public MapReader(RiffReader riffReader) 15 | { 16 | if (riffReader.Type != "GBMP" || riffReader.Version != SupportedVersion) 17 | { 18 | ThrowHelper.ThrowInvalidFileFormat(); 19 | } 20 | 21 | _riffReader = riffReader; 22 | } 23 | 24 | public Map Read() 25 | { 26 | // We're ignoring UMAP, CMAP PSXM chunks 27 | 28 | CompressedMap map; 29 | MapObject[] objects; 30 | MapZone[] zones; 31 | TileAnimation[] animations; 32 | MapLight[] lights; 33 | 34 | using (var compressedMapChunk = _riffReader.GetRequiredChunk("DMAP")) 35 | { 36 | map = ParseMap(compressedMapChunk); 37 | } 38 | 39 | using (var objectsChunk = _riffReader.GetChunk("MOBJ")) 40 | { 41 | objects = ParseArray(objectsChunk); 42 | } 43 | 44 | using (var zonesChunk = _riffReader.GetChunk("ZONE")) 45 | { 46 | zones = ParseMapZones(zonesChunk); 47 | } 48 | 49 | using (var animationsChunk = _riffReader.GetChunk("ANIM")) 50 | { 51 | animations = ParseAnimations(animationsChunk); 52 | } 53 | 54 | using (var junctionsChunk = _riffReader.GetChunk("RGEN")) 55 | { 56 | // TODO 57 | } 58 | 59 | using (var lightsChunk = _riffReader.GetChunk("LGHT")) 60 | { 61 | lights = ParseArray(lightsChunk); 62 | } 63 | 64 | return new Map(map, objects, zones, animations, lights); 65 | } 66 | 67 | private const int MapWidth = 256; 68 | private const int MapHeight = 256; 69 | 70 | private static TileAnimation[] ParseAnimations(RiffChunk? chunk) 71 | { 72 | if (chunk == null) 73 | return Array.Empty(); 74 | 75 | var stream = chunk.Stream; 76 | 77 | Span tilesBuffer = stackalloc byte[512]; 78 | 79 | var result = new List(); 80 | while (stream.Position < stream.Length) 81 | { 82 | stream.ReadExact(out TileAnimationHeader header); 83 | 84 | var tilesBytes = tilesBuffer[..(header.AnimLength * 2)]; 85 | stream.ReadExact(tilesBytes); 86 | var tiles = MemoryMarshal.Cast(tilesBytes); 87 | 88 | result.Add(new TileAnimation 89 | { 90 | Base = header.Base, 91 | FrameRate = header.FrameRate, 92 | Repeat = header.Repeat, 93 | Tiles = tiles.ToArray() 94 | }); 95 | } 96 | 97 | return result.ToArray(); 98 | } 99 | 100 | private static MapZone[] ParseMapZones(RiffChunk? chunk) 101 | { 102 | if (chunk == null) 103 | return Array.Empty(); 104 | 105 | var stream = chunk.Stream; 106 | 107 | Span nameBuffer = stackalloc byte[256]; 108 | 109 | var result = new List(); 110 | while (stream.Position < stream.Length) 111 | { 112 | stream.ReadExact(out MapZoneHeader header); 113 | 114 | var name = nameBuffer[..header.NameLength]; 115 | stream.ReadExact(name); 116 | 117 | result.Add(new MapZone 118 | { 119 | X = header.X, 120 | Y = header.Y, 121 | Width = header.Width, 122 | Height = header.Height, 123 | Type = header.Type, 124 | Name = Encoding.ASCII.GetString(name) 125 | }); 126 | } 127 | 128 | return result.ToArray(); 129 | } 130 | 131 | private static T[] ParseArray(RiffChunk? chunk) where T : struct 132 | { 133 | if (chunk == null) 134 | return Array.Empty(); 135 | 136 | var count = chunk.Stream.Length / Marshal.SizeOf(); 137 | 138 | var result = new T[count]; 139 | chunk.Stream.ReadExact(result.AsSpan()); 140 | 141 | return result; 142 | } 143 | 144 | private static CompressedMap ParseMap(RiffChunk chunk) 145 | { 146 | var stream = chunk.Stream; 147 | 148 | // the map data consists of the following structs (along with defined structs): 149 | // struct compressed_map 150 | // { 151 | // UInt32 base [256][256]; 152 | // UInt32 column_words; 153 | // UInt32 column[variable size – column_words]; // contains col_info structs 154 | // UInt32 num_blocks; 155 | // block_info block[variable size – num_blocks]; 156 | // } 157 | 158 | // struct col_info 159 | // { 160 | // UInt8 height; 161 | // UInt8 offset; 162 | // UInt16 pad; 163 | // UInt32 blockd[variable size – height - offset]; 164 | // } 165 | 166 | // read base 167 | 168 | var @base = new uint[MapHeight, MapWidth]; 169 | var baseSpan = MemoryMarshal.CreateSpan(ref Unsafe.As(ref MemoryMarshal.GetArrayDataReference(@base)), @base.Length); 170 | stream.ReadExact(baseSpan); 171 | 172 | // read columns 173 | var columnDoubleWords = stream.ReadExactDoubleWord(); 174 | var columnsStart = stream.Position; 175 | var columnsEnd = columnsStart + columnDoubleWords * 4; 176 | var columns = new Dictionary(); 177 | 178 | while (stream.Position < columnsEnd) 179 | { 180 | var columnOffset = stream.Position - columnsStart; 181 | 182 | var height = stream.ReadExactByte(); 183 | var offset = stream.ReadExactByte(); 184 | var blockIndices = new uint[height - offset]; 185 | 186 | // skip padding 187 | stream.ReadExactWord(); 188 | 189 | for (var i = 0; i < blockIndices.Length; i++) 190 | { 191 | blockIndices[i] = stream.ReadExactDoubleWord(); 192 | } 193 | 194 | columns[(uint)columnOffset / 4] = new ColumnInfo(height, offset, blockIndices); 195 | } 196 | 197 | // read blocks 198 | var blocks = new BlockInfo[stream.ReadExactDoubleWord()]; 199 | stream.ReadExact(blocks.AsSpan()); 200 | 201 | return new CompressedMap(@base, columns, blocks); 202 | } 203 | 204 | [StructLayout(LayoutKind.Explicit)] 205 | private struct TileAnimationHeader 206 | { 207 | [FieldOffset(0)] public readonly ushort Base; 208 | [FieldOffset(2)] public readonly byte FrameRate; 209 | [FieldOffset(3)] public readonly byte Repeat; 210 | [FieldOffset(4)] public readonly byte AnimLength; 211 | [FieldOffset(5)] private readonly byte _unused; 212 | } 213 | 214 | [StructLayout(LayoutKind.Explicit)] 215 | private struct MapZoneHeader 216 | { 217 | [FieldOffset(0)] public readonly ZoneType Type; 218 | [FieldOffset(1)] public readonly byte X; 219 | [FieldOffset(2)] public readonly byte Y; 220 | [FieldOffset(3)] public readonly byte Width; 221 | [FieldOffset(4)] public readonly byte Height; 222 | [FieldOffset(5)] public readonly byte NameLength; 223 | } 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /src/OpenGta2.GameData/Map/Models/Ang8.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace OpenGta2.GameData.Map; 4 | 5 | [StructLayout(LayoutKind.Explicit)] 6 | public struct Ang8 7 | { 8 | [FieldOffset(0)] 9 | public byte _data; 10 | } -------------------------------------------------------------------------------- /src/OpenGta2.GameData/Map/Models/Arrow.cs: -------------------------------------------------------------------------------- 1 | namespace OpenGta2.GameData.Map; 2 | 3 | [Flags] 4 | public enum Arrow : byte 5 | { 6 | GreenLeft = 1 << 0, 7 | GreenRight = 1 << 1, 8 | GreenUp = 1 << 2, 9 | GreenDown = 1 << 3, 10 | RedLeft = 1 << 4, 11 | RedRight = 1 << 5, 12 | RedUp = 1 << 6, 13 | RedDown = 1 << 7, 14 | } -------------------------------------------------------------------------------- /src/OpenGta2.GameData/Map/Models/BlockInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace OpenGta2.GameData.Map; 4 | 5 | [StructLayout(LayoutKind.Explicit)] 6 | public struct BlockInfo 7 | { 8 | [FieldOffset(0)] public FaceInfo Left; 9 | [FieldOffset(2)] public FaceInfo Right; 10 | [FieldOffset(4)] public FaceInfo Top; 11 | [FieldOffset(6)] public FaceInfo Bottom; 12 | [FieldOffset(8)] public FaceInfo Lid; 13 | [FieldOffset(10)] public Arrow Arrows; 14 | [FieldOffset(11)] public SlopeInfo SlopeType; 15 | } -------------------------------------------------------------------------------- /src/OpenGta2.GameData/Map/Models/ColorArgb.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace OpenGta2.GameData.Map; 4 | 5 | [StructLayout(LayoutKind.Explicit)] 6 | public readonly struct ColorArgb 7 | { 8 | [FieldOffset(0)] 9 | private readonly uint _data; 10 | 11 | public byte A => (byte)((_data >> 24) & 0xff); 12 | public byte R => (byte)((_data >> 16) & 0xff); 13 | public byte G => (byte)((_data >> 8) & 0xff); 14 | public byte B => (byte)(_data & 0xff); 15 | } -------------------------------------------------------------------------------- /src/OpenGta2.GameData/Map/Models/ColumnInfo.cs: -------------------------------------------------------------------------------- 1 | namespace OpenGta2.GameData.Map; 2 | 3 | /// 4 | /// 5 | /// The height of the column (0 to 7) 6 | /// The number of empty blocks at the bottom (0 to ). 7 | /// Block numbers for each block. 8 | public record ColumnInfo(byte Height, byte Offset, uint[] Blocks); -------------------------------------------------------------------------------- /src/OpenGta2.GameData/Map/Models/CompressedMap.cs: -------------------------------------------------------------------------------- 1 | namespace OpenGta2.GameData.Map; 2 | 3 | public record CompressedMap(uint[,] Base, Dictionary Columns, BlockInfo[] Blocks); -------------------------------------------------------------------------------- /src/OpenGta2.GameData/Map/Models/FaceInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace OpenGta2.GameData.Map; 5 | 6 | [StructLayout(LayoutKind.Explicit)] 7 | [DebuggerDisplay("TileGraphic = {TileGraphic}, Rotation = {Rotation}")] 8 | public readonly struct FaceInfo 9 | { 10 | [FieldOffset(0)] private readonly ushort _data; 11 | 12 | public FaceInfo(ushort data) => _data = data; 13 | 14 | public ushort TileGraphic => (ushort)(_data & 0b11_1111_1111); 15 | 16 | /// 17 | /// not on lid 18 | /// 19 | public bool Wall => (_data & (1 << 10)) == 1 << 10; 20 | /// 21 | /// not on lid 22 | /// 23 | public bool BulletWall => (_data & (1 << 11)) == 1 << 11; 24 | 25 | /// 26 | /// only on lid 27 | /// 28 | public byte LightingLevel => (byte)((_data >> 10) & 3); 29 | 30 | public bool Flat => (_data & (1 << 12)) == 1 << 12; 31 | public bool Flip => (_data & (1 << 13)) == 1 << 13; 32 | public Rotation Rotation => (Rotation)(byte)(_data >> 14); 33 | 34 | } -------------------------------------------------------------------------------- /src/OpenGta2.GameData/Map/Models/Fixed16.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace OpenGta2.GameData.Map; 5 | 6 | [StructLayout(LayoutKind.Explicit)] 7 | [DebuggerDisplay("{DebugDisplay}")] 8 | public struct Fixed16 9 | { 10 | [FieldOffset(0)] 11 | public short _data; 12 | 13 | private float DebugDisplay => this; 14 | 15 | public static implicit operator float(Fixed16 value) 16 | { 17 | int x = value._data; 18 | const int e = 7; 19 | var c = Math.Abs(x); 20 | var sign = 1; 21 | if (x < 0) //convert back from two's complement 22 | { 23 | c = x - 1; 24 | c = ~c; 25 | sign = -1; 26 | } 27 | 28 | var f = 1.0 * c / Math.Pow(2, e); 29 | f *= sign; 30 | 31 | return (float)f; 32 | } 33 | } -------------------------------------------------------------------------------- /src/OpenGta2.GameData/Map/Models/GroundType.cs: -------------------------------------------------------------------------------- 1 | namespace OpenGta2.GameData.Map; 2 | 3 | public enum GroundType : byte 4 | { 5 | Air = 0, 6 | Road = 1, 7 | Pavement = 2, 8 | Field = 3 9 | } -------------------------------------------------------------------------------- /src/OpenGta2.GameData/Map/Models/Junction.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace OpenGta2.GameData.Map; 4 | 5 | [StructLayout(LayoutKind.Explicit)] 6 | public struct Junction 7 | { 8 | [FieldOffset(0)] public JunctionLink North; 9 | [FieldOffset(4)] public JunctionLink South; 10 | [FieldOffset(8)] public JunctionLink East; 11 | [FieldOffset(12)] public JunctionLink West; 12 | [FieldOffset(16)] public byte JuncType;// TODO: is this the correct data type? 13 | [FieldOffset(17)] public byte MinX; 14 | [FieldOffset(18)] public byte MinY; 15 | [FieldOffset(19)] public byte MaxX; 16 | [FieldOffset(20)] public byte MaxY; 17 | } -------------------------------------------------------------------------------- /src/OpenGta2.GameData/Map/Models/JunctionLink.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace OpenGta2.GameData.Map; 4 | 5 | [StructLayout(LayoutKind.Explicit)] 6 | public struct JunctionLink 7 | { 8 | [FieldOffset(0)] 9 | public uint _data; 10 | } -------------------------------------------------------------------------------- /src/OpenGta2.GameData/Map/Models/JunctionSegment.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace OpenGta2.GameData.Map; 4 | 5 | [StructLayout(LayoutKind.Explicit)] 6 | public struct JunctionSegment 7 | { 8 | [FieldOffset(0)] public ushort JunctionNum1; 9 | [FieldOffset(2)] public ushort JunctionNum2; 10 | [FieldOffset(4)] public byte MinX; 11 | [FieldOffset(5)] public byte MinY; 12 | [FieldOffset(6)] public byte MaxX; 13 | [FieldOffset(7)] public byte MaxY; 14 | } -------------------------------------------------------------------------------- /src/OpenGta2.GameData/Map/Models/Map.cs: -------------------------------------------------------------------------------- 1 | namespace OpenGta2.GameData.Map; 2 | 3 | public record Map(CompressedMap CompressedMap, MapObject[] Objects, MapZone[] Zones, TileAnimation[] Animations, MapLight[] Lights) 4 | { 5 | public int Width { get; } = CompressedMap.Base.GetLength(1); 6 | public int Height { get; } = CompressedMap.Base.GetLength(0); 7 | 8 | public ColumnInfo GetColumn(int x, int y) 9 | { 10 | var num = CompressedMap.Base[y, x]; 11 | var column = CompressedMap.Columns[num]; 12 | return column; 13 | } 14 | 15 | public ref BlockInfo GetBlock(int x, int y, int z) 16 | { 17 | var column = GetColumn(x, y); 18 | 19 | if (z < column.Offset || z >= column.Height) 20 | { 21 | throw new ArgumentOutOfRangeException(nameof(z)); 22 | } 23 | 24 | return ref CompressedMap.Blocks[column.Blocks[z - column.Offset]]; 25 | } 26 | 27 | public BlockInfo? TryGetBlock(int x, int y, int z) 28 | { 29 | var column = GetColumn(x, y); 30 | 31 | if (z < column.Offset || z >= column.Height) 32 | { 33 | return null; 34 | } 35 | 36 | return CompressedMap.Blocks[column.Blocks[z - column.Offset]]; 37 | } 38 | 39 | public int GetGroundZ(int x, int y) 40 | { 41 | var col = GetColumn(x, y); 42 | 43 | for (var zo = 1; zo < col.Height - col.Offset; zo++) 44 | { 45 | var z = zo + col.Offset; 46 | var block = CompressedMap.Blocks[col.Blocks[zo]]; 47 | 48 | if (block.Lid.TileGraphic == 0) 49 | { 50 | return z; 51 | } 52 | } 53 | 54 | return col.Height; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/OpenGta2.GameData/Map/Models/MapLight.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace OpenGta2.GameData.Map; 4 | 5 | [StructLayout(LayoutKind.Explicit)] 6 | public struct MapLight 7 | { 8 | [FieldOffset(0)] public ColorArgb ARGB; 9 | [FieldOffset(4)] public Fixed16 X; 10 | [FieldOffset(6)] public Fixed16 Y; 11 | [FieldOffset(8)] public Fixed16 Z; 12 | [FieldOffset(10)] public Fixed16 Radius; 13 | [FieldOffset(12)] public byte Intensity; 14 | [FieldOffset(13)] public byte Shape; 15 | [FieldOffset(14)] public byte OnTime; 16 | [FieldOffset(15)] public byte OffTime; 17 | } -------------------------------------------------------------------------------- /src/OpenGta2.GameData/Map/Models/MapObject.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace OpenGta2.GameData.Map; 4 | 5 | [StructLayout(LayoutKind.Explicit)] 6 | public struct MapObject 7 | { 8 | [FieldOffset(0)] public Fixed16 X; 9 | [FieldOffset(2)] public Fixed16 Y; 10 | [FieldOffset(4)] public Ang8 Rotation; 11 | [FieldOffset(5)] public byte ObjectType; 12 | } -------------------------------------------------------------------------------- /src/OpenGta2.GameData/Map/Models/MapZone.cs: -------------------------------------------------------------------------------- 1 | namespace OpenGta2.GameData.Map; 2 | 3 | public struct MapZone 4 | { 5 | public ZoneType Type; 6 | public byte X; 7 | public byte Y; 8 | public byte Width; 9 | public byte Height; 10 | public string Name; 11 | } -------------------------------------------------------------------------------- /src/OpenGta2.GameData/Map/Models/Rotation.cs: -------------------------------------------------------------------------------- 1 | namespace OpenGta2.GameData.Map; 2 | 3 | public enum Rotation : byte 4 | { 5 | Rotate0 = 0, 6 | Rotate90 = 1, 7 | Rotate180 = 2, 8 | Rotate270 = 3 9 | } -------------------------------------------------------------------------------- /src/OpenGta2.GameData/Map/Models/SlopeInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace OpenGta2.GameData.Map; 4 | 5 | [StructLayout(LayoutKind.Explicit)] 6 | public readonly struct SlopeInfo 7 | { 8 | 9 | [FieldOffset(0)] private readonly byte _data; 10 | 11 | public GroundType GroundType => (GroundType)(_data & 0x3); 12 | 13 | public SlopeType SlopeType => (SlopeType)(byte)(_data >> 2); 14 | } -------------------------------------------------------------------------------- /src/OpenGta2.GameData/Map/Models/SlopeType.cs: -------------------------------------------------------------------------------- 1 | namespace OpenGta2.GameData.Map; 2 | 3 | public enum SlopeType : byte 4 | { 5 | None, 6 | Up26_1, 7 | Up26_2, 8 | Down26_1, 9 | Down26_2, 10 | Left26_1, 11 | Left26_2, 12 | Right26_1, 13 | Right26_2, 14 | Up7_1, 15 | Up7_2, 16 | Up7_3, 17 | Up7_4, 18 | Up7_5, 19 | Up7_6, 20 | Up7_7, 21 | Up7_8, 22 | Down7_1, 23 | Down7_2, 24 | Down7_3, 25 | Down7_4, 26 | Down7_5, 27 | Down7_6, 28 | Down7_7, 29 | Down7_8, 30 | Left7_1, 31 | Left7_2, 32 | Left7_3, 33 | Left7_4, 34 | Left7_5, 35 | Left7_6, 36 | Left7_7, 37 | Left7_8, 38 | Right7_1, 39 | Right7_2, 40 | Right7_3, 41 | Right7_4, 42 | Right7_5, 43 | Right7_6, 44 | Right7_7, 45 | Right7_8, 46 | Up45, 47 | Down45, 48 | Left45, 49 | Right45, 50 | DiagonalFacingUpLeft, 51 | DiagonalFacingUpRight, 52 | DiagonalFacingDownLeft, 53 | DiagonalFacingDownRight, 54 | DiagonalSlopeFacingUpLeft, 55 | DiagonalSlopeFacingUpRight, 56 | DiagonalSlopeFacingDownLeft, 57 | DiagonalSlopeFacingDownRight, 58 | PartialBlockLeft, 59 | PartialBlockRight, 60 | PartialBlockTop, 61 | PartialBlockBottom, 62 | PartialBlockTopLeftCorner, 63 | PartialBlockTopRightCorner, 64 | PartialBlockBottomRightCorner, 65 | PartialBlockBottomLeftCorner, 66 | PartialBlockCentre, 67 | Reserved, 68 | SlopeAbove 69 | } -------------------------------------------------------------------------------- /src/OpenGta2.GameData/Map/Models/TileAnimation.cs: -------------------------------------------------------------------------------- 1 | namespace OpenGta2.GameData.Map; 2 | 3 | public struct TileAnimation 4 | { 5 | public ushort Base; 6 | public byte FrameRate; 7 | public byte Repeat; 8 | public ushort[] Tiles; 9 | } -------------------------------------------------------------------------------- /src/OpenGta2.GameData/Map/Models/ZoneType.cs: -------------------------------------------------------------------------------- 1 | namespace OpenGta2.GameData.Map; 2 | 3 | public enum ZoneType : byte 4 | { 5 | GeneralPurpose = 0, 6 | Navigation = 1, 7 | TrafficLight = 2, 8 | ArrowBLocker = 5, 9 | RailwayPlatform = 6, 10 | BusStop = 7, 11 | GeneralTrigger = 8, 12 | Information = 10, 13 | RailwayStationEntry = 11, 14 | RailwayStationExit = 12, 15 | RailwayStop = 13, 16 | Gang = 14, 17 | LocalNavigation = 15, 18 | Restart = 16, 19 | ArrestRestart = 20 20 | } -------------------------------------------------------------------------------- /src/OpenGta2.GameData/OpenGta2.Data.csproj.DotSettings: -------------------------------------------------------------------------------- 1 |  2 | True 3 | True 4 | True -------------------------------------------------------------------------------- /src/OpenGta2.GameData/OpenGta2.GameData.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | True 8 | latest 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/OpenGta2.GameData/OpenGta2.GameData.csproj.DotSettings: -------------------------------------------------------------------------------- 1 |  2 | True 3 | True 4 | True -------------------------------------------------------------------------------- /src/OpenGta2.GameData/Riff/RiffChunk.cs: -------------------------------------------------------------------------------- 1 | namespace OpenGta2.GameData.Riff; 2 | 3 | public class RiffChunk : IDisposable 4 | { 5 | private readonly RiffChunkStream _stream; 6 | 7 | internal RiffChunk(string name, RiffChunkStream stream) 8 | { 9 | Name = name; 10 | _stream = stream; 11 | } 12 | 13 | public bool IsDisposed => _stream.IsDisposed; 14 | 15 | public string Name { get; } 16 | public Stream Stream => _stream; 17 | 18 | public void Dispose() 19 | { 20 | Stream.Dispose(); 21 | } 22 | } -------------------------------------------------------------------------------- /src/OpenGta2.GameData/Riff/RiffChunkNotFoundException.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.Serialization; 2 | 3 | namespace OpenGta2.GameData.Riff; 4 | 5 | [Serializable] 6 | public class RiffChunkNotFoundException : Exception 7 | { 8 | public RiffChunkNotFoundException(string chunkName) : base($"The chunk '{chunkName}' could not be found in the specified file.") 9 | { 10 | } 11 | 12 | protected RiffChunkNotFoundException(SerializationInfo info, 13 | StreamingContext context) : base(info, context) 14 | { 15 | } 16 | } -------------------------------------------------------------------------------- /src/OpenGta2.GameData/Riff/RiffChunkStream.cs: -------------------------------------------------------------------------------- 1 | namespace OpenGta2.GameData.Riff; 2 | 3 | public class RiffChunkStream : Stream 4 | { 5 | private readonly RiffReader _reader; 6 | private Stream? _innerStream; 7 | private readonly long _length; 8 | private long _position; 9 | private readonly long _start; 10 | 11 | internal RiffChunkStream(RiffReader reader, Stream innerStream, long length) 12 | { 13 | _reader = reader; 14 | _innerStream = innerStream; 15 | _length = length; 16 | 17 | if (_innerStream.CanSeek) 18 | { 19 | _start = _innerStream.Position; 20 | } 21 | } 22 | 23 | public bool IsDisposed => _innerStream == null; 24 | 25 | public override bool CanRead => true; 26 | 27 | public override bool CanSeek => _innerStream?.CanSeek ?? throw new ObjectDisposedException(nameof(RiffChunkStream)); 28 | 29 | public override bool CanWrite => false; 30 | 31 | public override long Length 32 | { 33 | get 34 | { 35 | EnsureNotDisposed(); 36 | return _length; 37 | } 38 | } 39 | 40 | public override long Position 41 | { 42 | get 43 | { 44 | EnsureNotDisposed(); 45 | return _position; 46 | } 47 | set => Seek(value, SeekOrigin.Begin); 48 | } 49 | 50 | private long RemainingLength => _length - _position; 51 | 52 | public override int Read(byte[] buffer, int offset, int count) 53 | { 54 | EnsureNotDisposed(); 55 | 56 | if (offset < 0) throw new ArgumentException("Offset should not be negative", nameof(offset)); 57 | if (count < 0) throw new ArgumentException("Count should not be negative", nameof(count)); 58 | if (offset + count > buffer.Length) throw new ArgumentException("Insufficient buffer size", nameof(buffer)); 59 | 60 | if (count > RemainingLength) 61 | { 62 | count = (int)RemainingLength; 63 | } 64 | 65 | var read = _innerStream!.Read(buffer, offset, count); 66 | 67 | _position += read; 68 | 69 | return read; 70 | } 71 | 72 | public override long Seek(long offset, SeekOrigin origin) 73 | { 74 | EnsureNotDisposed(); 75 | 76 | if (!_innerStream!.CanSeek) 77 | { 78 | throw new NotSupportedException(); 79 | } 80 | 81 | switch (origin) 82 | { 83 | case SeekOrigin.Begin: 84 | if (offset > _length || offset < 0) 85 | { 86 | throw new ArgumentOutOfRangeException(nameof(offset), offset, 87 | "Offset should be a positive number less than the length of the stream"); 88 | } 89 | 90 | _position = _innerStream.Seek(_start + offset, SeekOrigin.Begin) - _start; 91 | break; 92 | case SeekOrigin.Current: 93 | var target = _position + offset; 94 | if (target < 0 || target > _length) 95 | throw new ArgumentOutOfRangeException(nameof(offset), offset, "Seeking beyond stream end is not supported"); 96 | 97 | _position = _innerStream.Seek(offset, SeekOrigin.Current) - _start; 98 | break; 99 | case SeekOrigin.End: 100 | if (offset > 0 || -offset > _length) 101 | throw new ArgumentOutOfRangeException(nameof(offset), offset, 102 | "Offset should be a negative number les than the length of the stream"); 103 | 104 | _position = _innerStream.Seek(_start + _length, SeekOrigin.Begin) - _start; 105 | break; 106 | default: 107 | throw new InvalidOperationException(); 108 | } 109 | 110 | return _position; 111 | } 112 | 113 | public override int Read(Span buffer) 114 | { 115 | EnsureNotDisposed(); 116 | 117 | var remaining = RemainingLength; 118 | 119 | if (remaining == 0) 120 | return 0; 121 | 122 | if (buffer.Length > remaining) 123 | { 124 | buffer = buffer[..(int)remaining]; 125 | } 126 | 127 | var read = _innerStream!.Read(buffer); 128 | 129 | _position += read; 130 | 131 | return read; 132 | } 133 | 134 | public override int ReadByte() 135 | { 136 | EnsureNotDisposed(); 137 | 138 | if (_position >= _length) return -1; 139 | 140 | 141 | var result = _innerStream!.ReadByte(); 142 | 143 | if (result >= 0) 144 | { 145 | _position++; 146 | } 147 | 148 | return result; 149 | } 150 | 151 | public override void Flush() => throw new NotSupportedException(); 152 | 153 | public override void SetLength(long value) => throw new NotSupportedException(); 154 | 155 | public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); 156 | 157 | protected override void Dispose(bool disposing) 158 | { 159 | if (IsDisposed) 160 | { 161 | return; 162 | } 163 | 164 | if (CanSeek) 165 | { 166 | Seek(0, SeekOrigin.End); 167 | } 168 | else 169 | { 170 | var remainder = RemainingLength; 171 | 172 | if (remainder > 0) 173 | { 174 | Span buffer = stackalloc byte[256]; 175 | while (remainder > 0) 176 | { 177 | var read = Read(buffer); 178 | remainder -= read; 179 | 180 | if (read == 0) 181 | { 182 | throw new Exception("invalid read"); 183 | } 184 | } 185 | } 186 | } 187 | 188 | _innerStream = null; 189 | } 190 | 191 | private void EnsureNotDisposed() 192 | { 193 | if (_innerStream == null) throw new ObjectDisposedException(nameof(RiffChunkStream)); 194 | } 195 | } -------------------------------------------------------------------------------- /src/OpenGta2.GameData/Riff/RiffReader.cs: -------------------------------------------------------------------------------- 1 | namespace OpenGta2.GameData.Riff; 2 | 3 | public sealed class RiffReader : IDisposable 4 | { 5 | private const int TypeLength = 4; 6 | private const int ChunkNameLength = 4; 7 | private const int HeaderLength = 6; 8 | 9 | private readonly Stream _stream; 10 | private RiffChunkStream? _activeChunkStream; 11 | private bool _isDisposed; 12 | private readonly Dictionary _chunkCache = new(); 13 | private bool _fullCache; 14 | 15 | public RiffReader(Stream stream) 16 | { 17 | _stream = stream; 18 | Type = stream.ReadExactString(TypeLength); 19 | Version = stream.ReadExactWord(); 20 | } 21 | 22 | public string Type { get; } 23 | 24 | public ushort Version { get; } 25 | 26 | private void CloseActiveChunk() 27 | { 28 | // Disposing a chunk will fast-forward the stream to the end of the chunk. 29 | _activeChunkStream?.Dispose(); 30 | _activeChunkStream = null; 31 | } 32 | 33 | public RiffChunk? Next() 34 | { 35 | EnsureNotDisposed(); 36 | 37 | CloseActiveChunk(); 38 | 39 | var position = _stream.Position; 40 | 41 | var name = _stream.TryReadExactString(ChunkNameLength); 42 | 43 | if (name == null) 44 | { 45 | // We reached end of stream. This means we've cached all chunk offsets. 46 | _fullCache = true; 47 | 48 | return null; 49 | } 50 | 51 | var length = _stream.ReadExactDoubleWord(); 52 | 53 | var stream = new RiffChunkStream(this, _stream, length); 54 | _activeChunkStream = stream; 55 | 56 | // Cache chunk position for GetChunk calls. 57 | _chunkCache[name] = position; 58 | 59 | return new RiffChunk(name, stream); 60 | } 61 | 62 | public RiffChunk? GetChunk(string name) 63 | { 64 | EnsureNotDisposed(); 65 | 66 | if (!_stream.CanSeek) 67 | { 68 | throw new NotSupportedException(); 69 | } 70 | 71 | CloseActiveChunk(); 72 | 73 | // check cache 74 | if (_chunkCache.TryGetValue(name, out var position)) 75 | { 76 | _stream.Seek(position, SeekOrigin.Begin); 77 | return Next(); 78 | } 79 | 80 | if (_fullCache) 81 | { 82 | // All chunk positions have been cached - seeking won't help. 83 | return null; 84 | } 85 | 86 | while (Next() is { } current) 87 | { 88 | if (current.Name == name) 89 | { 90 | return current; 91 | } 92 | } 93 | 94 | return null; 95 | } 96 | 97 | public void Dispose() 98 | { 99 | CloseActiveChunk(); 100 | _isDisposed = true; 101 | } 102 | 103 | private void EnsureNotDisposed() 104 | { 105 | if (_isDisposed) 106 | { 107 | throw new ObjectDisposedException(nameof(RiffReader)); 108 | } 109 | } 110 | } -------------------------------------------------------------------------------- /src/OpenGta2.GameData/Riff/RiffReaderExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace OpenGta2.GameData.Riff; 2 | 3 | public static class RiffReaderExtensions 4 | { 5 | public static RiffChunk GetRequiredChunk(this RiffReader reader, string name) => 6 | reader.GetChunk(name) ?? throw new RiffChunkNotFoundException(name); 7 | 8 | public static RiffChunk GetRequiredChunk(this RiffReader reader, string name, long length) 9 | { 10 | var chunk = GetRequiredChunk(reader, name); 11 | 12 | if (chunk.Stream.Length != length) 13 | { 14 | ThrowHelper.ThrowInvalidFileFormat(); 15 | } 16 | 17 | return chunk; 18 | } 19 | } -------------------------------------------------------------------------------- /src/OpenGta2.GameData/Scripts/CommandParameters/SpawnCarParameters.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | using OpenGta2.GameData.Game; 3 | 4 | namespace OpenGta2.GameData.Scripts.CommandParameters; 5 | 6 | [StructLayout(LayoutKind.Explicit)] 7 | public struct SpawnCarParameters 8 | { 9 | [FieldOffset(0)] 10 | public ushort Variable; 11 | [FieldOffset(4)] 12 | public int X; 13 | [FieldOffset(8)] 14 | public int Y; 15 | [FieldOffset(12)] 16 | public int Z; 17 | [FieldOffset(16)] 18 | public ushort Rotation; 19 | [FieldOffset(18)] 20 | public short Remap; 21 | [FieldOffset(20)] 22 | public ushort Model; 23 | [FieldOffset(22)] 24 | public ushort Trailer; 25 | 26 | public Vector3 Position => Vector3.FromInt(X, Y, Z); 27 | } -------------------------------------------------------------------------------- /src/OpenGta2.GameData/Scripts/CommandParameters/SpawnPlayerPedParameters.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | using OpenGta2.GameData.Game; 3 | 4 | namespace OpenGta2.GameData.Scripts.CommandParameters; 5 | 6 | [StructLayout(LayoutKind.Explicit)] 7 | public struct SpawnPlayerPedParameters 8 | { 9 | [FieldOffset(4)] 10 | public int X; 11 | [FieldOffset(8)] 12 | public int Y; 13 | [FieldOffset(12)] 14 | public int Z; 15 | [FieldOffset(16)] 16 | public ushort Rotation; 17 | [FieldOffset(18)] 18 | public short Remap; 19 | 20 | public Vector3 Position => Vector3.FromInt(X, Y, Z); 21 | } -------------------------------------------------------------------------------- /src/OpenGta2.GameData/Scripts/Interpreting/IScriptRuntime.cs: -------------------------------------------------------------------------------- 1 | using OpenGta2.GameData.Scripts.CommandParameters; 2 | 3 | namespace OpenGta2.GameData.Scripts.Interpreting; 4 | 5 | public interface IScriptRuntime 6 | { 7 | void SpawnCar(ushort ptrIndex, ScriptCommandType type, SpawnCarParameters arguments); 8 | void SpawnPlayerPed(ushort ptrIndex, SpawnPlayerPedParameters arguments); 9 | 10 | // TODO: This method should eventually disappear when all commands have been implemented. 11 | void UnknownCommand(ushort ptrIndex, ScriptCommand command); 12 | } -------------------------------------------------------------------------------- /src/OpenGta2.GameData/Scripts/Interpreting/ScriptCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace OpenGta2.GameData.Scripts.Interpreting; 4 | 5 | [StructLayout(LayoutKind.Explicit)] 6 | public struct ScriptCommand 7 | { 8 | [FieldOffset(0)] public ushort PtrIndex; 9 | [FieldOffset(2)] public ScriptCommandType Type; 10 | [FieldOffset(4)] public ushort NextPtrIndex; 11 | [FieldOffset(6)] public ScriptCommandFlags Flags; 12 | } -------------------------------------------------------------------------------- /src/OpenGta2.GameData/Scripts/Interpreting/ScriptCommandFlags.cs: -------------------------------------------------------------------------------- 1 | namespace OpenGta2.GameData.Scripts.Interpreting; 2 | 3 | public enum ScriptCommandFlags : ushort 4 | { 5 | None, 6 | Execute 7 | } -------------------------------------------------------------------------------- /src/OpenGta2.GameData/Scripts/Interpreting/ScriptCommandType.cs: -------------------------------------------------------------------------------- 1 | namespace OpenGta2.GameData.Scripts.Interpreting; 2 | 3 | public enum ScriptCommandType : ushort 4 | { 5 | PLAYER_PED = 0x0005, 6 | ARROW_DATA = 0x0017, 7 | GENERATOR001C = 0x001C, 8 | GENERATOR001D = 0x001D, 9 | GENERATOR001E = 0x001E, 10 | GENERATOR001F = 0x001F, 11 | GENERATOR0020 = 0x0020, 12 | DESTRUCTOR = 0x23, 13 | CONVEYOR = 0x001b, 14 | CRANE_DATA0026 = 0x26, 15 | CRANE_DATA0027 = 0x27, 16 | CRUSHER = 0x0028, 17 | OBJ_DATA0014 = 0x0014, 18 | OBJ_DATA0012 = 0x0012, 19 | RADIO_STATION = 0x011f, 20 | CAR_DATA = 0x0009, 21 | DECLARE_POWERUP_CARLIST = 0x0161, 22 | DECLARE_CRANE_POWERUP = 0x01b7, 23 | PARKED_CAR_DATA = 0x01aa, 24 | SOUND = 0x0147, 25 | OBJ_DATA0010 = 0x0010, 26 | LEVELSTART = 0x003b, 27 | LEVELEND = 0x003c, 28 | EXEC = 0x003f, 29 | CREATE_GANG_CAR018a = 0x018a, 30 | CREATE_GANG_CAR018b = 0x018b, 31 | CREATE_GANG_CAR018c = 0x018c, 32 | CREATE_GANG_CAR018d = 0x018d, 33 | SET_AMBIENT_LEVEL = 0x00e2, 34 | SET_SHADING_LEVEL = 0x0175, 35 | ENABLE_CRANE = 0x00f8, 36 | PUT_CAR_ON_TRAILER = 0x013c, 37 | GIVE_WEAPON = 0x010a, 38 | SET_CAR_BULLETPROOF = 0x0137, 39 | SET_CAR_FLAMEPROOF = 0x0139, 40 | SET_CAR_ROCKETPROOF = 0x0138, 41 | SWITCH_GENERATOR = 0x010c, 42 | GIVE_CAR_ALARM = 0x0136, 43 | ENDEXEC = 0x0040, 44 | } 45 | -------------------------------------------------------------------------------- /src/OpenGta2.GameData/Scripts/Interpreting/ScriptInterpreter.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | using OpenGta2.GameData.Scripts.CommandParameters; 3 | 4 | namespace OpenGta2.GameData.Scripts.Interpreting; 5 | 6 | public class ScriptInterpreter 7 | { 8 | private static readonly int CommandSize = Marshal.SizeOf(); 9 | 10 | public void Run(Script script, IScriptRuntime runtime) 11 | { 12 | foreach (var pointer in script.Pointers) 13 | { 14 | if (pointer == 0) 15 | { 16 | continue; 17 | } 18 | 19 | var functionData = script.ScriptData.AsSpan(pointer..); 20 | var argsData = functionData[CommandSize..]; 21 | 22 | var command = MemoryMarshal.Read(functionData); 23 | 24 | switch (command.Type) 25 | { 26 | case ScriptCommandType.PARKED_CAR_DATA: 27 | { 28 | var args = MemoryMarshal.Read(argsData); 29 | runtime.SpawnCar(command.PtrIndex, command.Type, args); 30 | break; 31 | } 32 | case ScriptCommandType.PLAYER_PED: 33 | { 34 | var args = MemoryMarshal.Read(argsData); 35 | runtime.SpawnPlayerPed(command.PtrIndex, args); 36 | break; 37 | } 38 | // TODO: Handle other commands 39 | default: 40 | runtime.UnknownCommand(command.PtrIndex, command); 41 | break; 42 | } 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /src/OpenGta2.GameData/Scripts/Parsing/Script.cs: -------------------------------------------------------------------------------- 1 | namespace OpenGta2.GameData.Scripts; 2 | 3 | public class Script 4 | { 5 | public Script(ushort[] pointers, byte[] scriptData, StringTable strings) 6 | { 7 | Pointers = pointers; 8 | ScriptData = scriptData; 9 | Strings = strings; 10 | } 11 | 12 | public ushort[] Pointers { get; } 13 | public byte[] ScriptData { get; } 14 | public StringTable Strings { get; } 15 | } -------------------------------------------------------------------------------- /src/OpenGta2.GameData/Scripts/Parsing/ScriptParser.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace OpenGta2.GameData.Scripts; 4 | 5 | public class ScriptParser 6 | { 7 | public Script Parse(Stream stream) 8 | { 9 | // GTA2 script file format: 10 | // - POINTERS: 6000 word values which point to functions within the script 11 | // - SCRIPT: 65536 bytes of script binary data 12 | // - STRING TABLE: 13 | // - LENGTH: a word which indicates the length of the string table data 14 | // - STRINGS: 15 | // - TYPE: dword the type of the string 16 | // - LENGTH: a byte which contains the length of the string 17 | // - VALUE: a null terminated string value 18 | 19 | var pointers = ReadPointers(stream); 20 | var scriptData = ReadScript(stream); 21 | var strings = ReadStrings(stream); 22 | 23 | return new Script(pointers, scriptData, strings); 24 | } 25 | 26 | private static ushort[] ReadPointers(Stream stream) 27 | { 28 | var pointers = new ushort[6000]; 29 | stream.ReadExact(pointers.AsSpan()); 30 | return pointers; 31 | } 32 | 33 | private static byte[] ReadScript(Stream stream) 34 | { 35 | var scriptData = new byte[65536]; 36 | stream.ReadExact(scriptData.AsSpan()); 37 | return scriptData; 38 | } 39 | 40 | private static StringTable ReadStrings(Stream stream) 41 | { 42 | var tableLength = stream.ReadExactWord(); 43 | var read = 0; 44 | var strings = new Dictionary(); 45 | 46 | while (read < tableLength) 47 | { 48 | stream.ReadExact(out StringHeader header); 49 | var value = stream.ReadExactString(header.Length); 50 | 51 | strings[header.Id] = new StringValue(header.Type, header.Length, value); 52 | 53 | read += 9 + header.Length; 54 | } 55 | 56 | return new StringTable(strings); 57 | } 58 | 59 | [StructLayout(LayoutKind.Explicit)] 60 | private readonly struct StringHeader 61 | { 62 | [FieldOffset(0)] public readonly uint Id; 63 | [FieldOffset(4)] public readonly StringType Type; 64 | [FieldOffset(8)] public readonly byte Length; 65 | } 66 | } -------------------------------------------------------------------------------- /src/OpenGta2.GameData/Scripts/Parsing/StringTable.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | 3 | namespace OpenGta2.GameData.Scripts; 4 | 5 | public class StringTable : IEnumerable 6 | { 7 | public StringTable(IReadOnlyDictionary values) 8 | { 9 | Values = values; 10 | } 11 | 12 | public IReadOnlyDictionary Values { get; } 13 | public IEnumerator GetEnumerator() => Values.Values.GetEnumerator(); 14 | IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); 15 | } -------------------------------------------------------------------------------- /src/OpenGta2.GameData/Scripts/Parsing/StringType.cs: -------------------------------------------------------------------------------- 1 | namespace OpenGta2.GameData.Scripts; 2 | 3 | public enum StringType : uint 4 | { 5 | 6 | } -------------------------------------------------------------------------------- /src/OpenGta2.GameData/Scripts/Parsing/StringValue.cs: -------------------------------------------------------------------------------- 1 | namespace OpenGta2.GameData.Scripts; 2 | 3 | public record struct StringValue(StringType Type, byte Length, string Value); -------------------------------------------------------------------------------- /src/OpenGta2.GameData/StreamExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | using System.Text; 3 | 4 | namespace OpenGta2.GameData; 5 | 6 | internal static class StreamExtensions 7 | { 8 | public static string ReadExactString(this Stream stream, byte length) 9 | { 10 | return TryReadExactString(stream, length) ?? throw ThrowHelper.GetUnexpectedEndOfStream(); 11 | } 12 | 13 | public static string? TryReadExactString(this Stream stream, byte length) 14 | { 15 | Span buffer = stackalloc byte[length]; 16 | var read = stream.Read(buffer); 17 | 18 | if (read != length) 19 | { 20 | return null; 21 | } 22 | 23 | var nullTerminatorIndex = buffer.IndexOf((byte)0); 24 | 25 | if (nullTerminatorIndex < 0) 26 | { 27 | return Encoding.ASCII.GetString(buffer); 28 | } 29 | else 30 | { 31 | var textBuffer = buffer[..nullTerminatorIndex]; 32 | return Encoding.ASCII.GetString(textBuffer); 33 | } 34 | } 35 | 36 | public static unsafe ushort ReadExactWord(this Stream stream) 37 | { 38 | ushort d = 0; 39 | var p = (byte*)&d; 40 | var length = stream.Read(new Span(p, 2)); 41 | 42 | if (length != 2) 43 | { 44 | ThrowHelper.ThrowUnexpectedEndOfStream(); 45 | } 46 | return d; 47 | } 48 | 49 | public static unsafe uint ReadExactDoubleWord(this Stream stream) 50 | { 51 | uint d = 0; 52 | var p = (byte*)&d; 53 | var length = stream.Read(new Span(p, 4)); 54 | 55 | if (length != 4) 56 | { 57 | ThrowHelper.ThrowUnexpectedEndOfStream(); 58 | } 59 | 60 | return d; 61 | } 62 | 63 | public static int ReadExact(this Stream stream, Span span) 64 | { 65 | var read = stream.Read(span); 66 | 67 | if (read != span.Length) 68 | { 69 | ThrowHelper.ThrowUnexpectedEndOfStream(); 70 | } 71 | 72 | return read; 73 | } 74 | 75 | public static int ReadExact(this Stream stream, Span span) where T : struct 76 | { 77 | var buffer = MemoryMarshal.Cast(span); 78 | return ReadExact(stream, buffer) / Marshal.SizeOf(); 79 | } 80 | 81 | public static void ReadExact(this Stream stream, out T value) where T : struct 82 | { 83 | value = new T(); 84 | var span = MemoryMarshal.CreateSpan(ref value, 1); 85 | ReadExact(stream, span); 86 | } 87 | 88 | public static byte ReadExactByte(this Stream stream) 89 | { 90 | var b = stream.ReadByte(); 91 | 92 | if (b < 0) 93 | { 94 | ThrowHelper.ThrowUnexpectedEndOfStream(); 95 | } 96 | 97 | return (byte)b; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/OpenGta2.GameData/Style/Models/BgraColor.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace OpenGta2.GameData.Style; 4 | 5 | [StructLayout(LayoutKind.Explicit)] 6 | public struct BgraColor 7 | { 8 | [FieldOffset(0)] public byte B; 9 | [FieldOffset(1)] public byte G; 10 | [FieldOffset(2)] public byte R; 11 | [FieldOffset(3)] public byte A; 12 | 13 | public uint Argb => 14 | // alpha channel is unused 15 | (uint)(0xff000000u | (B << 16 | G << 8 | R)); 16 | } -------------------------------------------------------------------------------- /src/OpenGta2.GameData/Style/Models/FontBase.cs: -------------------------------------------------------------------------------- 1 | namespace OpenGta2.GameData.Style; 2 | 3 | public record struct FontBase(ushort[] Base) 4 | { 5 | public int FontCount => Base.Length; 6 | 7 | public int GetFontOffset(int index) 8 | { 9 | if (index < 0 || index >= FontCount) 10 | { 11 | throw new ArgumentOutOfRangeException(nameof(index)); 12 | } 13 | 14 | var offset = 0; 15 | for (var i = 0; i < index; i++) 16 | { 17 | offset += Base[i]; 18 | } 19 | 20 | return offset; 21 | } 22 | } -------------------------------------------------------------------------------- /src/OpenGta2.GameData/Style/Models/Palette.cs: -------------------------------------------------------------------------------- 1 | namespace OpenGta2.GameData.Style; 2 | 3 | public readonly ref struct Palette 4 | { 5 | private readonly PalettePage _page; 6 | private readonly byte _paletteNumber; 7 | 8 | public Palette(PalettePage page, byte paletteNumber) 9 | { 10 | _page = page; 11 | _paletteNumber = paletteNumber; 12 | } 13 | 14 | public BgraColor GetColor(byte entry) => _page.GetColor(_paletteNumber, entry); 15 | } -------------------------------------------------------------------------------- /src/OpenGta2.GameData/Style/Models/PaletteBase.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace OpenGta2.GameData.Style; 4 | 5 | [StructLayout(LayoutKind.Explicit)] 6 | public struct PaletteBase 7 | { 8 | [FieldOffset(0)] public ushort Tile; 9 | [FieldOffset(2)] public ushort Sprite; 10 | [FieldOffset(4)] public ushort CarRemap; 11 | [FieldOffset(6)] public ushort PedRemap; 12 | [FieldOffset(8)] public ushort CodeObjRemap; 13 | [FieldOffset(10)] public ushort MapObjRemap; 14 | [FieldOffset(12)] public ushort UserRemap; 15 | [FieldOffset(14)] public ushort FontRemap; 16 | 17 | public int TileOffset => 0; 18 | public int SpriteOffset => Tile; 19 | public int CarRemapOffset => SpriteOffset + Sprite; 20 | public int PedRemapOffset => CarRemapOffset + CarRemap; 21 | public int CodeObjRemapOffset => PedRemapOffset + CodeObjRemap; 22 | public int MapObjRemapOffset => CodeObjRemapOffset + CodeObjRemap; 23 | public int UserRemapOffset => MapObjRemapOffset + MapObjRemap; 24 | public int FontRemapOffset => UserRemapOffset + UserRemap; 25 | 26 | public int GetRemap(SpriteKind kind) 27 | { 28 | return kind switch 29 | { 30 | SpriteKind.Car => CarRemap, 31 | SpriteKind.Ped => PedRemap, 32 | SpriteKind.CodeObj => CodeObjRemap, 33 | SpriteKind.mapObj => MapObjRemap, 34 | SpriteKind.User => UserRemap, 35 | SpriteKind.Font => FontRemap, 36 | _ => throw new ArgumentOutOfRangeException(nameof(kind), kind, null) 37 | }; 38 | } 39 | public int GetRemapOffset(SpriteKind kind) 40 | { 41 | return kind switch 42 | { 43 | SpriteKind.Car => CarRemapOffset, 44 | SpriteKind.Ped => PedRemapOffset, 45 | SpriteKind.CodeObj => CodeObjRemapOffset, 46 | SpriteKind.mapObj => MapObjRemapOffset, 47 | SpriteKind.User => UserRemapOffset, 48 | SpriteKind.Font => FontRemapOffset, 49 | _ => throw new ArgumentOutOfRangeException(nameof(kind), kind, null) 50 | }; 51 | } 52 | } -------------------------------------------------------------------------------- /src/OpenGta2.GameData/Style/Models/PaletteIndex.cs: -------------------------------------------------------------------------------- 1 | namespace OpenGta2.GameData.Style; 2 | 3 | public struct PaletteIndex 4 | { 5 | public PaletteIndex(ushort[] physPalette) 6 | { 7 | if (physPalette.Length != PhysPaletteLength) 8 | { 9 | throw new ArgumentOutOfRangeException(nameof(physPalette), $"Length must be {PhysPaletteLength}"); 10 | } 11 | PhysPalette = physPalette; 12 | } 13 | 14 | public const int PhysPaletteLength = 16384; 15 | public ushort[] PhysPalette { get; } 16 | } -------------------------------------------------------------------------------- /src/OpenGta2.GameData/Style/Models/PalettePage.cs: -------------------------------------------------------------------------------- 1 | namespace OpenGta2.GameData.Style; 2 | 3 | public readonly ref struct PalettePage 4 | { 5 | private readonly Span _data; 6 | 7 | public PalettePage(Span data) 8 | { 9 | _data = data; 10 | } 11 | 12 | public BgraColor GetColor(byte paletteNumber, byte entry) => _data[paletteNumber + entry * PhysicalPalette.PalettesPerPage]; 13 | } -------------------------------------------------------------------------------- /src/OpenGta2.GameData/Style/Models/PhysicalPalette.cs: -------------------------------------------------------------------------------- 1 | namespace OpenGta2.GameData.Style; 2 | 3 | public readonly struct PhysicalPalette 4 | { 5 | public const int PaletteLength = 256; 6 | public const int PalettesPerPage = 64; 7 | public const int PalettePageLength = PaletteLength * PalettesPerPage; 8 | 9 | private readonly BgraColor[] _palette; 10 | 11 | public PhysicalPalette(BgraColor[] palette) 12 | { 13 | _palette = palette; 14 | } 15 | 16 | public int Count => _palette.Length / PaletteLength; 17 | public int PageCount => Count / PalettesPerPage; 18 | 19 | public PalettePage GetPage(ushort number) 20 | { 21 | if (number >= PageCount) 22 | { 23 | throw new ArgumentOutOfRangeException(nameof(number)); 24 | } 25 | 26 | var idx = number * PalettePageLength; 27 | 28 | return new PalettePage(_palette.AsSpan(idx, PalettePageLength)); 29 | } 30 | 31 | public Palette GetPalette(ushort number) 32 | { 33 | 34 | if (number >= Count) 35 | { 36 | throw new ArgumentOutOfRangeException(nameof(number)); 37 | } 38 | 39 | var pageNumber = number / PalettesPerPage; 40 | var paletteNumber = number - pageNumber * PalettesPerPage; 41 | 42 | var page = GetPage((ushort)pageNumber); 43 | return new Palette(page, (byte)paletteNumber); 44 | } 45 | } -------------------------------------------------------------------------------- /src/OpenGta2.GameData/Style/Models/Sprite.cs: -------------------------------------------------------------------------------- 1 | namespace OpenGta2.GameData.Style; 2 | 3 | public readonly struct Sprite 4 | { 5 | private readonly byte[] _pageData; 6 | private readonly SpriteEntry _entry; 7 | 8 | public Sprite(byte[] pageData, SpriteEntry entry, ushort number) 9 | { 10 | Number = number; 11 | _pageData = pageData; 12 | _entry = entry; 13 | } 14 | 15 | public byte this[byte y, byte x] => GetPixel(x, y); 16 | 17 | public ushort Number { get; } 18 | public byte Width => _entry.Width; 19 | public byte Height => _entry.Height; 20 | 21 | public byte GetPixel(byte x, byte y) 22 | { 23 | if (x >= _entry.Width) 24 | throw new ArgumentOutOfRangeException(nameof(x)); 25 | if (y >= _entry.Height) 26 | throw new ArgumentOutOfRangeException(nameof(y)); 27 | 28 | var py = _entry.PageY + y; 29 | var px = _entry.PageX + x; 30 | return _pageData[py * 256 + px]; 31 | } 32 | } -------------------------------------------------------------------------------- /src/OpenGta2.GameData/Style/Models/SpriteBase.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace OpenGta2.GameData.Style; 4 | 5 | [StructLayout(LayoutKind.Explicit)] 6 | public struct SpriteBase 7 | { 8 | [FieldOffset(0)] public ushort Car; 9 | [FieldOffset(2)] public ushort Ped; 10 | [FieldOffset(4)] public ushort CodeObj; 11 | [FieldOffset(6)] public ushort MapObj; 12 | [FieldOffset(8)] public ushort User; 13 | [FieldOffset(10)] public ushort Font; 14 | 15 | public int CarOffset => 0; 16 | public int PedOffset => Car; 17 | public int CodeObjOffset => PedOffset + Ped; 18 | public int MapObjOffset => CodeObjOffset + CodeObj; 19 | public int UserOffset => MapObjOffset + MapObj; 20 | public int FontOffset => UserOffset + User; 21 | 22 | public int GetOffset(SpriteKind kind) 23 | { 24 | return kind switch 25 | { 26 | SpriteKind.Car => CarOffset, 27 | SpriteKind.Ped => PedOffset, 28 | SpriteKind.CodeObj => CodeObjOffset, 29 | SpriteKind.mapObj => MapObjOffset, 30 | SpriteKind.User => UserOffset, 31 | SpriteKind.Font => FontOffset, 32 | _ => throw new ArgumentOutOfRangeException(nameof(kind), kind, null) 33 | }; 34 | } 35 | } -------------------------------------------------------------------------------- /src/OpenGta2.GameData/Style/Models/SpriteEntry.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace OpenGta2.GameData.Style; 4 | 5 | [StructLayout(LayoutKind.Explicit)] 6 | public struct SpriteEntry 7 | { 8 | [FieldOffset(0)] public uint Ptr; 9 | [FieldOffset(4)] public byte Width; 10 | [FieldOffset(5)] public byte Height; 11 | [FieldOffset(6)] public ushort Pad; 12 | 13 | public uint PageNumber => Ptr / (256 * 256); 14 | public uint PageY => (Ptr - PageNumber * 256 * 256) / 256; 15 | public uint PageX => (Ptr - PageNumber * 256 * 256) % 256; 16 | } -------------------------------------------------------------------------------- /src/OpenGta2.GameData/Style/Models/SpriteKind.cs: -------------------------------------------------------------------------------- 1 | namespace OpenGta2.GameData.Style; 2 | 3 | public enum SpriteKind : byte 4 | { 5 | Car, 6 | Ped, 7 | CodeObj, 8 | mapObj, 9 | User, 10 | Font 11 | } -------------------------------------------------------------------------------- /src/OpenGta2.GameData/Style/Models/SpritePage.cs: -------------------------------------------------------------------------------- 1 | namespace OpenGta2.GameData.Style; 2 | 3 | public readonly struct SpritePage 4 | { 5 | private readonly byte[] _data; 6 | 7 | public SpritePage(byte[] data) 8 | { 9 | _data = data; 10 | } 11 | 12 | public Sprite GetSprite(SpriteEntry entry, ushort number) 13 | { 14 | return new Sprite(_data, entry, number); 15 | } 16 | } -------------------------------------------------------------------------------- /src/OpenGta2.GameData/Style/Models/Style.cs: -------------------------------------------------------------------------------- 1 | namespace OpenGta2.GameData.Style; 2 | 3 | public record Style(PaletteBase PaletteBase, PaletteIndex PaletteIndex, PhysicalPalette PhysicsalPalette, Tiles Tiles, SpritePage[] SpriteGraphics, SpriteBase SpriteBases, SpriteEntry[] SpriteEntries, FontBase FontBase); -------------------------------------------------------------------------------- /src/OpenGta2.GameData/Style/Models/Tile.cs: -------------------------------------------------------------------------------- 1 | namespace OpenGta2.GameData.Style; 2 | 3 | public readonly ref struct Tile 4 | { 5 | public const int Width = 64; 6 | public const int Height = 64; 7 | public const int Size = Width * Height; 8 | 9 | private readonly TilesPage _page; 10 | private readonly int _index; 11 | 12 | public Tile(TilesPage page, int index) 13 | { 14 | _page = page; 15 | _index = index; 16 | } 17 | 18 | public byte this[byte y, byte x] => GetPixel(x, y); 19 | 20 | public byte GetPixel(byte x, byte y) 21 | { 22 | if (x >= Width) throw new ArgumentOutOfRangeException(nameof(x)); 23 | if (y >= Height) throw new ArgumentOutOfRangeException(nameof(y)); 24 | 25 | var tx = _index % 4; 26 | var ty = _index / 4; 27 | 28 | var px = tx * Width + x; 29 | var py = ty * Height + y; 30 | 31 | return _page.Data[py * TilesPage.Width + px]; 32 | } 33 | } -------------------------------------------------------------------------------- /src/OpenGta2.GameData/Style/Models/Tiles.cs: -------------------------------------------------------------------------------- 1 | namespace OpenGta2.GameData.Style; 2 | 3 | public readonly struct Tiles 4 | { 5 | private readonly byte[] _data; 6 | 7 | public Tiles(byte[] data) 8 | { 9 | _data = data; 10 | } 11 | 12 | public int TileCount => _data.Length / Tile.Size; 13 | 14 | public int PageCount => _data.Length / TilesPage.Size; 15 | 16 | public TilesPage GetPage(int index) 17 | { 18 | if (index < 0 || index >= PageCount) throw new ArgumentOutOfRangeException(nameof(index)); 19 | 20 | var page = _data.AsSpan(index * TilesPage.Size, TilesPage.Size); 21 | return new TilesPage(page); 22 | } 23 | 24 | public Tile GetTile(int index) 25 | { 26 | if (index < 0 || index >= TileCount) throw new ArgumentOutOfRangeException(nameof(index)); 27 | 28 | var page = index / TilesPage.TilesPerPage; 29 | var indexOnPage = index - page * TilesPage.TilesPerPage; 30 | 31 | return GetPage(page).GetTile(indexOnPage); 32 | } 33 | } -------------------------------------------------------------------------------- /src/OpenGta2.GameData/Style/Models/TilesPage.cs: -------------------------------------------------------------------------------- 1 | namespace OpenGta2.GameData.Style; 2 | 3 | public readonly ref struct TilesPage 4 | { 5 | public const int Width = Tile.Width * 4; 6 | public const int Height = Tile.Height * 4; 7 | public const int TilesPerPage = 4 * 4; 8 | public const int Size = Width * Height; 9 | 10 | private readonly Span _data; 11 | 12 | public TilesPage(Span data) 13 | { 14 | _data = data; 15 | } 16 | 17 | public Span Data => _data; 18 | 19 | public Tile GetTile(int index) 20 | { 21 | if(index is < 0 or >= TilesPerPage) 22 | throw new ArgumentOutOfRangeException(nameof(index)); 23 | 24 | return new Tile(this, index); 25 | } 26 | } -------------------------------------------------------------------------------- /src/OpenGta2.GameData/Style/StyleReader.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | using OpenGta2.GameData.Riff; 3 | 4 | namespace OpenGta2.GameData.Style 5 | { 6 | public class StyleReader 7 | { 8 | private const int SupportedVersion = 700; 9 | 10 | private readonly RiffReader _riffReader; 11 | 12 | public StyleReader(RiffReader riffReader) 13 | { 14 | if (riffReader.Type != "GBST" || riffReader.Version != SupportedVersion) 15 | { 16 | ThrowHelper.ThrowInvalidFileFormat(); 17 | } 18 | 19 | _riffReader = riffReader; 20 | } 21 | 22 | public Style Read() 23 | { 24 | var paletteBase = ReadPaletteBase(); 25 | var paletteIndex = ReadPaletteIndex(); 26 | var physPalette = ReadPhysicalPalettes(); 27 | var tiles = ReadTiles(); 28 | 29 | var spriteGraphics = ReadSpriteGraphics(); 30 | var spriteBases = ReadSpriteBases(); 31 | var spriteIndex = ReadSpriteIndex(); 32 | 33 | var fontBase = ReadFontBase(); 34 | 35 | return new Style(paletteBase, paletteIndex, physPalette, tiles, spriteGraphics, spriteBases, spriteIndex, fontBase); 36 | } 37 | 38 | private PaletteBase ReadPaletteBase() 39 | { 40 | using var chunk = _riffReader.GetRequiredChunk("PALB", Marshal.SizeOf()); 41 | chunk.Stream.ReadExact(out PaletteBase result); 42 | return result; 43 | } 44 | 45 | private PaletteIndex ReadPaletteIndex() 46 | { 47 | using var chunk = _riffReader.GetRequiredChunk("PALX", PaletteIndex.PhysPaletteLength * 2); 48 | 49 | var physPalette = new ushort[PaletteIndex.PhysPaletteLength]; 50 | chunk.Stream.ReadExact(physPalette.AsSpan()); 51 | 52 | return new PaletteIndex(physPalette); 53 | } 54 | 55 | private PhysicalPalette ReadPhysicalPalettes() 56 | { 57 | var chunk = _riffReader.GetRequiredChunk("PPAL"); 58 | 59 | var paletteSize = chunk.Stream.Length / Marshal.SizeOf(); 60 | var palette = new BgraColor[paletteSize]; 61 | chunk.Stream.ReadExact(palette.AsSpan()); 62 | 63 | return new PhysicalPalette(palette); 64 | } 65 | 66 | private FontBase ReadFontBase() 67 | { 68 | using var chunk = _riffReader.GetRequiredChunk("FONB"); 69 | 70 | var count = chunk.Stream.ReadExactWord(); 71 | 72 | var data = new ushort[count]; 73 | chunk.Stream.ReadExact(data.AsSpan()); 74 | 75 | return new FontBase(data); 76 | } 77 | private Tiles ReadTiles() 78 | { 79 | using var chunk = _riffReader.GetRequiredChunk("TILE"); 80 | 81 | var data = new byte[chunk.Stream.Length]; 82 | chunk.Stream.ReadExact(data); 83 | 84 | return new Tiles(data); 85 | } 86 | 87 | private SpritePage[] ReadSpriteGraphics() 88 | { 89 | using var chunk = _riffReader.GetRequiredChunk("SPRG"); 90 | 91 | var lengthCheck = chunk.Stream.Length % (256 * 256); 92 | 93 | if (lengthCheck != 0) 94 | { 95 | throw new InvalidOperationException($"Unexpected chunk length of {chunk.Stream.Length}. Is offset by {lengthCheck}"); 96 | } 97 | 98 | const int PageSize = 256 * 256; 99 | 100 | var pageCount = chunk.Stream.Length / PageSize; 101 | 102 | var pages = new SpritePage[pageCount]; 103 | 104 | for (var page = 0; page < pageCount; page++) 105 | { 106 | var data = new byte[PageSize]; 107 | chunk.Stream.ReadExact(data.AsSpan()); 108 | pages[page] = new SpritePage(data); 109 | } 110 | 111 | return pages; 112 | } 113 | 114 | private SpriteEntry[] ReadSpriteIndex() 115 | { 116 | using var chunk = _riffReader.GetRequiredChunk("SPRX"); 117 | 118 | var spriteCount = chunk.Stream.Length / Marshal.SizeOf(); 119 | 120 | var result = new SpriteEntry[spriteCount]; 121 | chunk.Stream.ReadExact(result.AsSpan()); 122 | 123 | return result; 124 | } 125 | 126 | private SpriteBase ReadSpriteBases() 127 | { 128 | using var chunk = _riffReader.GetRequiredChunk("SPRB", Marshal.SizeOf()); 129 | chunk.Stream.ReadExact(out SpriteBase result); 130 | return result; 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/OpenGta2.GameData/ThrowHelper.cs: -------------------------------------------------------------------------------- 1 | namespace OpenGta2.GameData; 2 | 3 | internal static class ThrowHelper 4 | { 5 | public static void ThrowInvalidFileFormat() => throw GetInvalidFileFormat(); 6 | 7 | public static Exception GetInvalidFileFormat() => new InvalidDataException("The specified file is not of a supported format."); 8 | 9 | public static void ThrowUnexpectedEndOfStream() => throw GetUnexpectedEndOfStream(); 10 | 11 | public static Exception GetUnexpectedEndOfStream() => new InvalidDataException("Unexpected end of the input stream."); 12 | } 13 | 14 | --------------------------------------------------------------------------------