├── .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 |
--------------------------------------------------------------------------------