├── .clang-format ├── .gitignore ├── CITATION.cff ├── CMakeLists.txt ├── Cover.jpg ├── LICENSE ├── README.md ├── Start-OnlineGame.ps1 ├── apps ├── CMakeLists.txt ├── plant │ ├── CMakeLists.txt │ └── dllmain.cpp └── zombie │ ├── CMakeLists.txt │ └── dllmain.cpp ├── build.ps1 ├── docs ├── Key Data and Functions.md ├── badges │ ├── C++.svg │ ├── License-MIT.svg │ ├── MASM.svg │ ├── Made-with-CMake.svg │ ├── Made-with-Visual-Studio.svg │ └── Microsoft-Windows.svg └── images │ ├── continue-or-new-game.png │ ├── i-zombie-endless.png │ ├── online-battle.gif │ └── puzzle-mode.png ├── game ├── Fix 'Fatal Error' on Windows 10.md ├── Plants vs. Zombies Chinese Version.zip └── userdata │ ├── user1.dat │ └── users.dat ├── include ├── game │ ├── config.h │ └── startup.h ├── network │ ├── ip_addr.h │ ├── listener.h │ ├── packet.h │ └── socket │ │ ├── basic.h │ │ └── tcp.h └── system │ ├── memory.h │ └── windows_error.h ├── online_config.ini └── src ├── CMakeLists.txt ├── game ├── CMakeLists.txt ├── config.cpp ├── mod │ ├── hook │ │ ├── hook.cpp │ │ ├── hook.h │ │ ├── interface.cpp │ │ ├── interface.h │ │ ├── net_packet.cpp │ │ └── net_packet.h │ ├── interface.cpp │ ├── interface.h │ ├── mod.cpp │ └── mod.h ├── startup.cpp ├── state.cpp └── state.h ├── network ├── CMakeLists.txt ├── ip_addr.cpp ├── packet.cpp └── socket │ └── basic.cpp └── system ├── CMakeLists.txt ├── memory.cpp └── windows_error.cpp /.clang-format: -------------------------------------------------------------------------------- 1 | BasedOnStyle: Google 2 | 3 | Standard: Latest 4 | 5 | ColumnLimit: 80 6 | 7 | DeriveLineEnding: false 8 | UseCRLF: false 9 | 10 | AlignAfterOpenBracket: Align 11 | AlignConsecutiveAssignments: false 12 | AlignConsecutiveDeclarations: false 13 | AlignConsecutiveMacros: false 14 | AlignEscapedNewlines: Left 15 | AlignTrailingComments: true 16 | AlignOperands: true 17 | 18 | AllowAllArgumentsOnNextLine: true 19 | AllowAllConstructorInitializersOnNextLine: true 20 | AllowAllParametersOfDeclarationOnNextLine: true 21 | AllowShortBlocksOnASingleLine: Never 22 | AllowShortCaseLabelsOnASingleLine: false 23 | AllowShortFunctionsOnASingleLine: Empty 24 | AllowShortIfStatementsOnASingleLine: Never 25 | AllowShortLambdasOnASingleLine: Inline 26 | AllowShortLoopsOnASingleLine: false 27 | 28 | AlwaysBreakAfterReturnType: None 29 | AlwaysBreakBeforeMultilineStrings: false 30 | AlwaysBreakTemplateDeclarations: Yes 31 | BinPackArguments: true 32 | BinPackParameters: true 33 | BreakBeforeBinaryOperators: NonAssignment 34 | BreakBeforeBraces: Attach 35 | BreakBeforeTernaryOperators: true 36 | BreakConstructorInitializers: AfterColon 37 | BreakInheritanceList: AfterColon 38 | BreakStringLiterals: true 39 | CompactNamespaces: false 40 | ConstructorInitializerAllOnOneLineOrOnePerLine: true 41 | DerivePointerAlignment: false 42 | PointerAlignment: Left 43 | 44 | IncludeBlocks: Preserve 45 | IndentCaseLabels: true 46 | IndentGotoLabels: true 47 | IndentPPDirectives: BeforeHash 48 | IndentWrappedFunctionNames: false 49 | NamespaceIndentation: None 50 | AccessModifierOffset: -4 51 | IndentWidth: 4 52 | ContinuationIndentWidth: 4 53 | ConstructorInitializerIndentWidth: 4 54 | TabWidth: 4 55 | UseTab: Never 56 | 57 | KeepEmptyLinesAtTheStartOfBlocks: false 58 | MaxEmptyLinesToKeep: 2 59 | ReflowComments: false 60 | FixNamespaceComments: true 61 | SortIncludes: true 62 | SortUsingDeclarations: true 63 | 64 | SpaceAfterCStyleCast: false 65 | Cpp11BracedListStyle: false 66 | SpaceAfterLogicalNot: false 67 | SpaceAfterTemplateKeyword: true 68 | SpaceBeforeAssignmentOperators: true 69 | SpaceBeforeCpp11BracedList: false 70 | SpaceBeforeCtorInitializerColon: true 71 | SpaceBeforeInheritanceColon: true 72 | SpaceBeforeParens: ControlStatements 73 | SpaceBeforeRangeBasedForLoopColon: true 74 | SpaceInEmptyBlock: false 75 | SpaceInEmptyParentheses: false 76 | SpacesBeforeTrailingComments: 2 77 | SpacesInAngles: false 78 | SpacesInCStyleCastParentheses: false 79 | SpacesInConditionalStatement: false 80 | SpacesInContainerLiterals: false 81 | SpacesInParentheses: false 82 | SpacesInSquareBrackets: false -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # #################################################################### 2 | # For C 3 | # #################################################################### 4 | # Prerequisites 5 | *.d 6 | 7 | # Object files 8 | *.o 9 | *.ko 10 | *.obj 11 | *.elf 12 | 13 | # Linker output 14 | *.ilk 15 | *.map 16 | *.exp 17 | 18 | # Precompiled Headers 19 | *.gch 20 | *.pch 21 | 22 | # Libraries 23 | *.lib 24 | *.a 25 | *.la 26 | *.lo 27 | 28 | # Shared objects (inc. Windows DLLs) 29 | *.dll 30 | *.so 31 | *.so.* 32 | *.dylib 33 | 34 | # Executables 35 | *.exe 36 | *.out 37 | *.app 38 | *.i*86 39 | *.x86_64 40 | *.hex 41 | 42 | # Debug files 43 | *.dSYM/ 44 | *.su 45 | *.idb 46 | *.pdb 47 | 48 | # Kernel Module Compile Results 49 | *.mod* 50 | *.cmd 51 | .tmp_versions/ 52 | modules.order 53 | Module.symvers 54 | Mkfile.old 55 | dkms.conf 56 | 57 | 58 | # #################################################################### 59 | # For CMake 60 | # #################################################################### 61 | CMakeLists.txt.user 62 | CMakeCache.txt 63 | CMakeFiles 64 | CMakeScripts 65 | Testing 66 | Makefile 67 | cmake_install.cmake 68 | install_manifest.txt 69 | compile_commands.json 70 | CTestTestfile.cmake 71 | _deps 72 | 73 | 74 | # #################################################################### 75 | # For Visual Studio Code 76 | # #################################################################### 77 | .vscode/* 78 | *.code-workspace 79 | 80 | 81 | # #################################################################### 82 | # For Visual Studio 83 | # Ignore temporary files, build results, and 84 | # files generated by popular Visual Studio add-ons. 85 | # #################################################################### 86 | # User-specific files 87 | *.rsuser 88 | *.suo 89 | *.user 90 | *.userosscache 91 | *.sln.docstates 92 | 93 | # User-specific files (MonoDevelop/Xamarin Studio) 94 | *.userprefs 95 | 96 | # Mono auto generated files 97 | mono_crash.* 98 | 99 | # Build results 100 | [Dd]ebug/ 101 | [Dd]ebugPublic/ 102 | [Rr]elease/ 103 | [Rr]eleases/ 104 | x64/ 105 | x86/ 106 | [Aa][Rr][Mm]/ 107 | [Aa][Rr][Mm]64/ 108 | bld/ 109 | [Bb]in/ 110 | [Oo]bj/ 111 | [Ll]og/ 112 | [Ll]ogs/ 113 | 114 | # Visual Studio 2015/2017 cache/options directory 115 | .vs/ 116 | # Uncomment if you have tasks that create the project's static files in wwwroot 117 | #wwwroot/ 118 | 119 | # Visual Studio 2017 auto generated files 120 | Generated\ Files/ 121 | 122 | # MSTest test Results 123 | [Tt]est[Rr]esult*/ 124 | [Bb]uild[Ll]og.* 125 | 126 | # NUnit 127 | *.VisualState.xml 128 | TestResult.xml 129 | nunit-*.xml 130 | 131 | # Build Results of an ATL Project 132 | [Dd]ebugPS/ 133 | [Rr]eleasePS/ 134 | dlldata.c 135 | 136 | # Benchmark Results 137 | BenchmarkDotNet.Artifacts/ 138 | 139 | # .NET Core 140 | project.lock.json 141 | project.fragment.lock.json 142 | artifacts/ 143 | 144 | # StyleCop 145 | StyleCopReport.xml 146 | 147 | # Files built by Visual Studio 148 | *_i.c 149 | *_p.c 150 | *_h.h 151 | *.ilk 152 | *.meta 153 | *.obj 154 | *.iobj 155 | *.pch 156 | *.pdb 157 | *.ipdb 158 | *.pgc 159 | *.pgd 160 | *.rsp 161 | *.sbr 162 | *.tlb 163 | *.tli 164 | *.tlh 165 | *.tmp 166 | *.tmp_proj 167 | *_wpftmp.csproj 168 | *.log 169 | *.vspscc 170 | *.vssscc 171 | .builds 172 | *.pidb 173 | *.svclog 174 | *.scc 175 | 176 | # Chutzpah Test files 177 | _Chutzpah* 178 | 179 | # Visual C++ cache files 180 | ipch/ 181 | *.aps 182 | *.ncb 183 | *.opendb 184 | *.opensdf 185 | *.sdf 186 | *.cachefile 187 | *.VC.db 188 | *.VC.VC.opendb 189 | 190 | # Visual Studio profiler 191 | *.psess 192 | *.vsp 193 | *.vspx 194 | *.sap 195 | 196 | # Visual Studio Trace Files 197 | *.e2e 198 | 199 | # TFS 2012 Local Workspace 200 | $tf/ 201 | 202 | # Guidance Automation Toolkit 203 | *.gpState 204 | 205 | # ReSharper is a .NET coding add-in 206 | _ReSharper*/ 207 | *.[Rr]e[Ss]harper 208 | *.DotSettings.user 209 | 210 | # TeamCity is a build add-in 211 | _TeamCity* 212 | 213 | # DotCover is a Code Coverage Tool 214 | *.dotCover 215 | 216 | # AxoCover is a Code Coverage Tool 217 | .axoCover/* 218 | !.axoCover/settings.json 219 | 220 | # Coverlet is a free, cross platform Code Coverage Tool 221 | coverage*[.json, .xml, .info] 222 | 223 | # Visual Studio code coverage results 224 | *.coverage 225 | *.coveragexml 226 | 227 | # NCrunch 228 | _NCrunch_* 229 | .*crunch*.local.xml 230 | nCrunchTemp_* 231 | 232 | # MightyMoose 233 | *.mm.* 234 | AutoTest.Net/ 235 | 236 | # Web workbench (sass) 237 | .sass-cache/ 238 | 239 | # Installshield output folder 240 | [Ee]xpress/ 241 | 242 | # DocProject is a documentation generator add-in 243 | DocProject/buildhelp/ 244 | DocProject/Help/*.HxT 245 | DocProject/Help/*.HxC 246 | DocProject/Help/*.hhc 247 | DocProject/Help/*.hhk 248 | DocProject/Help/*.hhp 249 | DocProject/Help/Html2 250 | DocProject/Help/html 251 | 252 | # Click-Once directory 253 | publish/ 254 | 255 | # Publish Web Output 256 | *.[Pp]ublish.xml 257 | *.azurePubxml 258 | # Note: Comment the next line if you want to checkin your web deploy settings, 259 | # but database connection strings (with potential passwords) will be unencrypted 260 | *.pubxml 261 | *.publishproj 262 | 263 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 264 | # checkin your Azure Web App publish settings, but sensitive information contained 265 | # in these scripts will be unencrypted 266 | PublishScripts/ 267 | 268 | # NuGet Packages 269 | *.nupkg 270 | # NuGet Symbol Packages 271 | *.snupkg 272 | # The packages folder can be ignored because of Package Restore 273 | **/[Pp]ackages/* 274 | # except build/, which is used as an MSBuild target. 275 | !**/[Pp]ackages/build/ 276 | # Uncomment if necessary however generally it will be regenerated when needed 277 | #!**/[Pp]ackages/repositories.config 278 | # NuGet v3's project.json files produces more ignorable files 279 | *.nuget.props 280 | *.nuget.targets 281 | 282 | # Microsoft Azure Build Output 283 | csx/ 284 | *.build.csdef 285 | 286 | # Microsoft Azure Emulator 287 | ecf/ 288 | rcf/ 289 | 290 | # Windows Store app package directories and files 291 | AppPackages/ 292 | BundleArtifacts/ 293 | Package.StoreAssociation.xml 294 | _pkginfo.txt 295 | *.appx 296 | *.appxbundle 297 | *.appxupload 298 | 299 | # Visual Studio cache files 300 | # files ending in .cache can be ignored 301 | *.[Cc]ache 302 | # but keep track of directories ending in .cache 303 | !?*.[Cc]ache/ 304 | 305 | # Others 306 | ClientBin/ 307 | ~$* 308 | *~ 309 | *.dbmdl 310 | *.dbproj.schemaview 311 | *.jfm 312 | *.pfx 313 | *.publishsettings 314 | orleans.codegen.cs 315 | 316 | # Including strong name files can present a security risk 317 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 318 | #*.snk 319 | 320 | # Since there are multiple workflows, uncomment next line to ignore bower_components 321 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 322 | #bower_components/ 323 | 324 | # RIA/Silverlight projects 325 | Generated_Code/ 326 | 327 | # Backup & report files from converting an old project file 328 | # to a newer Visual Studio version. Backup files are not needed, 329 | # because we have git ;-) 330 | _UpgradeReport_Files/ 331 | Backup*/ 332 | UpgradeLog*.XML 333 | UpgradeLog*.htm 334 | ServiceFabricBackup/ 335 | *.rptproj.bak 336 | 337 | # SQL Server files 338 | *.mdf 339 | *.ldf 340 | *.ndf 341 | 342 | # Business Intelligence projects 343 | *.rdl.data 344 | *.bim.layout 345 | *.bim_*.settings 346 | *.rptproj.rsuser 347 | *- [Bb]ackup.rdl 348 | *- [Bb]ackup ([0-9]).rdl 349 | *- [Bb]ackup ([0-9][0-9]).rdl 350 | 351 | # Microsoft Fakes 352 | FakesAssemblies/ 353 | 354 | # GhostDoc plugin setting file 355 | *.GhostDoc.xml 356 | 357 | # Node.js Tools for Visual Studio 358 | .ntvs_analysis.dat 359 | node_modules/ 360 | 361 | # Visual Studio 6 build log 362 | *.plg 363 | 364 | # Visual Studio 6 workspace options file 365 | *.opt 366 | 367 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 368 | *.vbw 369 | 370 | # Visual Studio LightSwitch build output 371 | **/*.HTMLClient/GeneratedArtifacts 372 | **/*.DesktopClient/GeneratedArtifacts 373 | **/*.DesktopClient/ModelManifest.xml 374 | **/*.Server/GeneratedArtifacts 375 | **/*.Server/ModelManifest.xml 376 | _Pvt_Extensions 377 | 378 | # Paket dependency manager 379 | .paket/paket.exe 380 | paket-files/ 381 | 382 | # FAKE - F# Make 383 | .fake/ 384 | 385 | # CodeRush personal settings 386 | .cr/personal 387 | 388 | # Python Tools for Visual Studio (PTVS) 389 | __pycache__/ 390 | *.pyc 391 | 392 | # Cake - Uncomment if you are using it 393 | # tools/** 394 | # !tools/packages.config 395 | 396 | # Tabs Studio 397 | *.tss 398 | 399 | # Telerik's JustMock configuration file 400 | *.jmconfig 401 | 402 | # BizTalk build output 403 | *.btp.cs 404 | *.btm.cs 405 | *.odx.cs 406 | *.xsd.cs 407 | 408 | # OpenCover UI analysis results 409 | OpenCover/ 410 | 411 | # Azure Stream Analytics local run output 412 | ASALocalRun/ 413 | 414 | # MSBuild Binary and Structured Log 415 | *.binlog 416 | 417 | # NVidia Nsight GPU debugger configuration file 418 | *.nvuser 419 | 420 | # MFractors (Xamarin productivity tool) working folder 421 | .mfractor/ 422 | 423 | # Local History for Visual Studio 424 | .localhistory/ 425 | 426 | # BeatPulse healthcheck temp database 427 | healthchecksdb 428 | 429 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 430 | MigrationBackup/ 431 | 432 | # Ionide (cross platform F# VS Code tools) working folder 433 | .ionide/ -------------------------------------------------------------------------------- /CITATION.cff: -------------------------------------------------------------------------------- 1 | cff-version: 1.2.0 2 | authors: 3 | - family-names: Chen 4 | given-names: Zhenshuo 5 | orcid: https://orcid.org/0000-0003-2091-4160 6 | - family-names: Liu 7 | given-names: Guowen 8 | orcid: https://orcid.org/0000-0002-8375-5729 9 | title: Plants vs. Zombies Online Battle 10 | date-released: 2021-08-04 11 | url: https://github.com/Zhuagenborn/Plants-vs.-Zombies-Online-Battle -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.21.0) 2 | 3 | cmake_policy(SET CMP0091 NEW) 4 | set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") 5 | 6 | project(Plants-vs-Zombies-Online-Battle LANGUAGES CXX) 7 | 8 | if((NOT CMAKE_SYSTEM_NAME STREQUAL "Windows") OR (NOT CMAKE_SIZEOF_VOID_P EQUAL 4)) 9 | message(FATAL_ERROR "Must configuring on/for Windows 32-bit") 10 | endif() 11 | 12 | set(CMAKE_CXX_STANDARD 23) 13 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 14 | 15 | set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/lib) 16 | set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/lib) 17 | set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/bin) 18 | 19 | add_subdirectory(src) 20 | add_subdirectory(apps) -------------------------------------------------------------------------------- /Cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zhuagenborn/Plants-vs.-Zombies-Online-Battle/42a2cc613560f31a28559ed85e8d807201dd8121/Cover.jpg -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Zhuagenborn 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # *Plants vs. Zombies* Online Battle 2 | 3 | ![C++](docs/badges/C++.svg) 4 | ![MASM](docs/badges/MASM.svg) 5 | [![CMake](docs/badges/Made-with-CMake.svg)](https://cmake.org) 6 | [![Visual-Studio](docs/badges/Made-with-Visual-Studio.svg)](https://visualstudio.microsoft.com/) 7 | [![Windows](docs/badges/Microsoft-Windows.svg)](https://www.microsoft.com/en-ie/windows) 8 | ![License](docs/badges/License-MIT.svg) 9 | 10 | ## Introduction 11 | 12 | ![Cover](Cover.jpg) 13 | 14 | This project adds the multiplayer battle to ***Plants vs. Zombies*** via reverse engineering, inline hook and dynamic-link library injection. Two online players can defend and attack as *Plant* and *Zombie* respectively. 15 | 16 | ## Getting Started 17 | 18 | ### Prerequisites 19 | 20 | - Install [*Visual Studio 2022*](https://visualstudio.microsoft.com). 21 | - Install [*CMake*](https://cmake.org). 22 | - Set the `PATH` environment variables. 23 | 24 | ### Building 25 | 26 | Set the location to the project folder and run: 27 | 28 | ```bash 29 | mkdir -p build 30 | cd build 31 | cmake .. -G "Visual Studio 17 2022" -A Win32 32 | cmake --build . 33 | ``` 34 | 35 | Two dynamic-link libraries `plant.dll` and `zombie.dll` will be generated in `build/bin` folder. Copy them to the game root folder. 36 | 37 | #### IPv6 38 | 39 | The default *IP* version is *IPv4*. Enable the following statement in `libs/game/CMakeLists.txt` if you want to build *IPv6* libraries. 40 | 41 | ```cmake 42 | target_compile_definitions(game PRIVATE INET6=1) 43 | ``` 44 | 45 | ## Usage 46 | 47 | > The project only works with *Plants vs. Zombies **1.0.0.1051 CHINESE*** version, provided in `game` folder. 48 | > 49 | > The *MD5* of `PlantsVsZombies.exe` is `37B729B4056131722A556E646AC915E9`. 50 | 51 | In order to activate online functions, `plant.dll` and `zombie.dll` must be injected into the game when it starts. You can directly use this simple injection tool: [*Dll-Injector*](https://github.com/Zhuagenborn/Windows-DLL-Injector). 52 | 53 | ![online-battle](docs/images/online-battle.gif) 54 | 55 | ### Playing as *Plant* 56 | 57 | If a player plays as the plant, the game will launch as a server. 58 | 59 | ```bash 60 | Dll-Injector -f PlantsVsZombies.exe plant.dll 61 | ``` 62 | 63 | Or use `Start-OnlineGame.ps1` directly. It needs to be copied to the game root folder. 64 | 65 | ```powershell 66 | Start-OnlineGame.ps1 -Role Plant 67 | ``` 68 | 69 | Start *I, Zombie Endless* level, the game will pause and wait for a client to connect. 70 | 71 | ### Playing as *Zombie* 72 | 73 | If a player plays as the zombie, the game will launch as a client. 74 | 75 | ```bash 76 | Dll-Injector -f PlantsVsZombies.exe zombie.dll 77 | ``` 78 | 79 | Or use `Start-OnlineGame.ps1`. 80 | 81 | ```powershell 82 | Start-OnlineGame.ps1 -Role Zombie 83 | ``` 84 | 85 | Start *I, Zombie Endless* level, the game will try to connect to the server. 86 | 87 | ### *I, Zombie Endless* 88 | 89 | If the modification has been loaded successfully, ***I, Zombie Endless*** level will be converted into an online level. If your current progress does not have this level, you can copy `game/userdata` to `C:\ProgramData\PopCap Games\PlantsVsZombies\userdata`. Remember to back up your own save-files before copying. 90 | 91 | In the Chinese version of the game, you can start this level as follows: 92 | 93 | 1. Select "*解谜模式*", which means "*Puzzle*" in English. 94 | 95 | ![puzzle-mode](docs/images/puzzle-mode.png) 96 | 97 | 2. Select "*我是僵尸无尽版*", the final level. It is "*I, Zombie Endless*" in English. 98 | 99 | ![i-zombie-endless](docs/images/i-zombie-endless.png) 100 | 101 | 3. The game will display a dialog box shown as below. Select "*继续*", the left button, meaning "*Continue*" in English. 102 | 103 | ![continue-or-new-game](docs/images/continue-or-new-game.png) 104 | 105 | Before starting an online battle, the progress of this level must be empty. Otherwise the battlefields of two online players will be different. If that happens, you need to restart the game without the modification, enter this level again but select "*新游戏*", the right button, to reset the progress. It means "*New Game*". 106 | 107 | ### Configurations 108 | 109 | Copy `online_config.ini` to the game root folder. You can set the server's IP address and port number in it. 110 | 111 | ```ini 112 | [Network] 113 | ServerIP=127.0.0.1 114 | Port=10000 115 | ``` 116 | 117 | ## Documents 118 | 119 | The code comment style follows the [*Doxygen*](http://www.doxygen.nl) specification. 120 | 121 | `docs/Key Data and Functions.md` describes key data and functions obtained by reverse engineering. 122 | 123 | ### Class Diagram 124 | 125 | ```mermaid 126 | classDiagram 127 | 128 | namespace state { 129 | 130 | class Role { 131 | <> 132 | Plante 133 | Zombie 134 | } 135 | 136 | class Config { 137 | vector~int~ zombies 138 | vector~int~ plants 139 | string server_ip 140 | int port 141 | } 142 | 143 | class State 144 | } 145 | 146 | State --> Role 147 | State --> Config 148 | State --> TcpSocket 149 | 150 | namespace network { 151 | 152 | class IpAddr { 153 | <> 154 | Version() int 155 | } 156 | 157 | class Ipv4Addr 158 | class Ipv6Addr 159 | 160 | class Socket { 161 | SetAddr(IpAddr) 162 | Bind() 163 | Close() 164 | } 165 | 166 | class TcpSocket { 167 | <<>> 168 | Connect(IpAddr) 169 | Send(data) size 170 | Recv(buffer) size 171 | } 172 | 173 | class Listener { 174 | Bind(IpAddr) 175 | Listen() 176 | Accept() TcpSocket 177 | Close() 178 | } 179 | 180 | class Packet { 181 | Recv(TcpSocket)$ Packet 182 | Send(TcpSocket) 183 | Write(data) size 184 | Read() data 185 | } 186 | } 187 | 188 | IpAddr <|.. Ipv4Addr 189 | IpAddr <|.. Ipv6Addr 190 | Socket --> IpAddr 191 | Socket <|-- TcpSocket 192 | Listener --> TcpSocket 193 | Packet ..> TcpSocket 194 | 195 | class Mod { 196 | <> 197 | Enable() 198 | Disable() 199 | } 200 | 201 | class ModLoader { 202 | Add(Mod) 203 | Load() 204 | } 205 | 206 | ModLoader o-- Mod 207 | 208 | class RemoveDefaultPlants 209 | Mod <|.. RemoveDefaultPlants 210 | 211 | class SetSunAmount 212 | Mod <|.. SetSunAmount 213 | 214 | class Hook { 215 | <> 216 | #From() address 217 | #To() address 218 | #JumpRet() optional~address~ 219 | } 220 | 221 | Mod <|-- Hook 222 | 223 | class BeforeLoadLevel 224 | Hook <|.. BeforeLoadLevel 225 | BeforeLoadLevel ..> CreateZombie 226 | BeforeLoadLevel ..> CreatePlant 227 | BeforeLoadLevel ..> LevelEnd 228 | BeforeLoadLevel ..> Listener 229 | BeforeLoadLevel ..> State 230 | 231 | class AfterLoadLevel 232 | Hook <|.. AfterLoadLevel 233 | AfterLoadLevel ..> RemoveDefaultPlants 234 | AfterLoadLevel ..> InitSlots 235 | 236 | class InitSlots 237 | Hook <|.. InitSlots 238 | InitSlots ..> State 239 | 240 | class LevelEnd 241 | Hook <|.. LevelEnd 242 | LevelEnd ..> State 243 | LevelEnd ..> Packet 244 | 245 | class CreateZombie 246 | Hook <|.. CreateZombie 247 | CreateZombie ..> State 248 | CreateZombie ..> Packet 249 | 250 | class CreatePlant 251 | Hook <|.. CreatePlant 252 | CreatePlant ..> State 253 | CreatePlant ..> Packet 254 | 255 | class Startup { 256 | Run() 257 | Stop() 258 | } 259 | 260 | Startup --> ModLoader 261 | Startup --> State 262 | ``` 263 | 264 | ## Issues & Bugs 265 | 266 | - The game sometimes crashes when creating zombies. 267 | - If the progress of *I, Zombie Endless* level is not empty, two players will have different battlefields. 268 | - In order to simplify the code, the running menu and automatic pause are disabled. 269 | 270 | ## License 271 | 272 | Distributed under the *MIT License*. See `LICENSE` for more information. -------------------------------------------------------------------------------- /Start-OnlineGame.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Start an online Plants vs. Zombies battle. 4 | .DESCRIPTION 5 | Start Plants vs. Zombies with dynamic-link library injection. 6 | Two online players defend and attack as the plant side and zombie side respectively.. 7 | .PARAMETER Role 8 | The player's role, which can be "Plant" or "Zombie". 9 | .EXAMPLE 10 | PS> .\Startup.ps1 -Role 'Plant' 11 | .LINK 12 | https://github.com/Zhuagenborn/Plants-vs.-Zombies-Online-Battle 13 | #> 14 | 15 | [CmdletBinding()] 16 | param ( 17 | [Parameter(Mandatory)] 18 | [ValidateSet('Plant', 'Zombie')] 19 | [string]$Role 20 | ) 21 | 22 | try { 23 | Start-Process -FilePath 'Dll-Injector' -ArgumentList '-f', 'PlantsVsZombies.exe', "$($Role.ToLower()).dll" 24 | } catch { 25 | Write-Host $_ -ForegroundColor Red 26 | } -------------------------------------------------------------------------------- /apps/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_subdirectory(plant) 2 | add_subdirectory(zombie) -------------------------------------------------------------------------------- /apps/plant/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_library(plant SHARED dllmain.cpp) 2 | target_link_libraries(plant PRIVATE game) -------------------------------------------------------------------------------- /apps/plant/dllmain.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file dllmain.cpp 3 | * @brief The @p DllMain of the plant side. 4 | * 5 | * @author Chen Zhenshuo (chenzs108@outlook.com) 6 | * @author Liu Guowen (liu.guowen@outlook.com) 7 | * @version 1.0 8 | * @date 2021-07-10 9 | * @par GitHub 10 | * https://github.com/czs108 11 | * @par 12 | * https://github.com/lgw1995 13 | */ 14 | 15 | #define _WINSOCKAPI_ 16 | 17 | #include "game/config.h" 18 | #include "game/startup.h" 19 | 20 | #include 21 | 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | 28 | 29 | using namespace game; 30 | 31 | namespace { 32 | 33 | constexpr std::string_view cfg_file{ "online_config.ini" }; 34 | 35 | extern std::unique_ptr startup; 36 | 37 | } // namespace 38 | 39 | 40 | BOOL APIENTRY DllMain(HMODULE, const DWORD reason, LPVOID) { 41 | try { 42 | switch (reason) { 43 | case DLL_PROCESS_ATTACH: { 44 | startup = std::make_unique( 45 | Role::Plant, 46 | Config{ std::filesystem::absolute(cfg_file).string() }); 47 | startup->Run(); 48 | break; 49 | } 50 | case DLL_PROCESS_DETACH: { 51 | startup.reset(); 52 | break; 53 | } 54 | default: { 55 | break; 56 | } 57 | } 58 | 59 | return TRUE; 60 | 61 | } catch (const std::exception& err) { 62 | const auto msg{ std::format( 63 | "Failed to load the online modification: {}", err.what()) }; 64 | OutputDebugStringA(msg.c_str()); 65 | return FALSE; 66 | } 67 | } 68 | 69 | 70 | namespace { 71 | std::unique_ptr startup{}; 72 | } // namespace -------------------------------------------------------------------------------- /apps/zombie/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_library(zombie SHARED dllmain.cpp) 2 | target_link_libraries(zombie PRIVATE game) -------------------------------------------------------------------------------- /apps/zombie/dllmain.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file dllmain.cpp 3 | * @brief The @p DllMain of the zombie side. 4 | * 5 | * @author Chen Zhenshuo (chenzs108@outlook.com) 6 | * @author Liu Guowen (liu.guowen@outlook.com) 7 | * @version 1.0 8 | * @date 2021-07-10 9 | * @par GitHub 10 | * https://github.com/czs108 11 | * @par 12 | * https://github.com/lgw1995 13 | */ 14 | 15 | #define _WINSOCKAPI_ 16 | 17 | #include "game/config.h" 18 | #include "game/startup.h" 19 | 20 | #include 21 | 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | 28 | 29 | using namespace game; 30 | 31 | namespace { 32 | 33 | constexpr std::string_view cfg_file{ "online_config.ini" }; 34 | 35 | extern std::unique_ptr startup; 36 | 37 | } // namespace 38 | 39 | 40 | BOOL APIENTRY DllMain(HMODULE, const DWORD reason, LPVOID) { 41 | try { 42 | switch (reason) { 43 | case DLL_PROCESS_ATTACH: { 44 | startup = std::make_unique( 45 | Role::Zombie, 46 | Config{ std::filesystem::absolute(cfg_file).string() }); 47 | startup->Run(); 48 | break; 49 | } 50 | case DLL_PROCESS_DETACH: { 51 | startup.reset(); 52 | break; 53 | } 54 | default: { 55 | break; 56 | } 57 | } 58 | 59 | return TRUE; 60 | 61 | } catch (const std::exception& err) { 62 | const auto msg{ std::format( 63 | "Failed to load the online modification: {}", err.what()) }; 64 | OutputDebugStringA(msg.c_str()); 65 | return FALSE; 66 | } 67 | } 68 | 69 | 70 | namespace { 71 | std::unique_ptr startup{}; 72 | } // namespace -------------------------------------------------------------------------------- /build.ps1: -------------------------------------------------------------------------------- 1 | New-Item -Name 'build' -ItemType 'Directory' -Force 2 | Set-Location -Path 'build' 3 | 4 | Start-Process -FilePath 'cmake' -ArgumentList '..', '-G "Visual Studio 17 2022"', '-A Win32' -NoNewWindow -Wait 5 | Start-Process -FilePath 'cmake' -ArgumentList '--build', '.' -NoNewWindow -Wait -------------------------------------------------------------------------------- /docs/Key Data and Functions.md: -------------------------------------------------------------------------------- 1 | # Key Data and Functions 2 | 3 | All information can be obtained through reverse engineering using the following tools: 4 | 5 | - [*Cheat Engine*](https://www.cheatengine.org): An open-source memory scanner. 6 | - [*IDA Pro*](https://hex-rays.com/ida-pro): A powerful disassembler and versatile debugger. 7 | - [*x64dbg*](https://x64dbg.com): An open-source binary debugger for *Windows*. 8 | 9 | ## Warning 10 | 11 | > This document is only for *Plants vs. Zombies **1.0.0.1051 CHINESE*** version. 12 | > 13 | > The *MD5* of `PlantsVsZombies.exe` is `37B729B4056131722A556E646AC915E9`. 14 | 15 | ## Data 16 | 17 | ### Slots 18 | 19 | Set a breakpoint at `0x004897B2`. Each time the game is interrupted while loading a new level, the 4-bit content of `[ESP]` will be the address of the slot structure that the game is displaying currently. 20 | 21 | ```c++ 22 | //! The slot. 23 | struct Slot { 24 | std::byte unknown1[8]; 25 | 26 | /** 27 | * @brief The X-coordinate on the screen. 28 | * 29 | * @par Offset 30 | * @p 0x8 31 | */ 32 | std::int32_t screen_pos_x; 33 | 34 | /** 35 | * @brief The Y-coordinate on the screen. 36 | * 37 | * @par Offset 38 | * @p 0xC 39 | */ 40 | std::int32_t screen_pos_y; 41 | 42 | std::byte unknown2[36]; 43 | 44 | /** 45 | * @brief The item ID. 46 | * 47 | * @details 48 | * If the type of the slot is zombie, this field is equal to the zombie ID plus @p 0x3C. 49 | * If the type of the slot is plant, this field is equal to the plant ID. 50 | * 51 | * @par Offset 52 | * @p 0x34 53 | */ 54 | std::int32_t id; 55 | }; 56 | ``` 57 | 58 | ### The Level 59 | 60 | - **The global base address** 61 | 62 | ```assembly 63 | mov ebx, 006A9F38h 64 | ``` 65 | 66 | - **The base address of the level environment** 67 | 68 | ```assembly 69 | mov ebx, dword ptr [006A9F38h] 70 | mov ebx, dword ptr [ebx + 768h] 71 | ``` 72 | 73 | - **The amount of sun** 74 | 75 | ```assembly 76 | mov ebx, dword ptr [006A9F38h] 77 | mov ebx, dword ptr [ebx + 768h] 78 | mov eax, dword ptr [ebx + 5560h] 79 | ``` 80 | 81 | - **The number of zombies** 82 | 83 | ```assembly 84 | mov ebx, dword ptr [006A9F38h] 85 | mov ebx, dword ptr [ebx + 768h] 86 | mov eax, dword ptr [ebx + 0A0h] 87 | ``` 88 | 89 | - **The number of planted plants** 90 | 91 | ```assembly 92 | mov ebx, dword ptr [006A9F38h] 93 | mov ebx, dword ptr [ebx + 768h] 94 | mov eax, dword ptr [ebx + 0B0h] 95 | ``` 96 | 97 | - **The base address of all planted plant structures** 98 | 99 | ```assembly 100 | mov ebx, dword ptr [006A9F38h] 101 | mov ebx, dword ptr [ebx + 768h] 102 | mov ebx, dword ptr [ebx + 0ACh] 103 | ``` 104 | 105 | The type of this structure is `PlantedPlant`. 106 | 107 | - **The countdown** 108 | 109 | ```assembly 110 | mov ebx, dword ptr [006A9F38h] 111 | mov ebx, dword ptr [ebx + 768h] 112 | mov eax, dword ptr [ebx + 5600h] 113 | ``` 114 | 115 | ### Plants 116 | 117 | - **Static plant information** 118 | 119 | ```c++ 120 | /** 121 | * @brief Static plant information. 122 | * 123 | * @par Binary Address 124 | * @p 0x0069F2B0 125 | */ 126 | struct Plant { 127 | /** 128 | * @brief The ID. 129 | * 130 | * @par Offset 131 | * @p 0x0 132 | */ 133 | std::int32_t id; 134 | 135 | std::byte unknown1[4]; 136 | 137 | /** 138 | * @brief The image resource. 139 | * 140 | * @par Offset 141 | * @p 0x8 142 | */ 143 | std::int32_t ui; 144 | 145 | std::byte unknown2[4]; 146 | 147 | /** 148 | * @brief The cost to plant. 149 | * 150 | * @par Offset 151 | * @p 0x10 152 | */ 153 | std::int32_t sun_cost; 154 | 155 | /** 156 | * @brief The recharge time. 157 | * 158 | * @par Offset 159 | * @p 0x14 160 | */ 161 | std::int32_t recharge_time; 162 | 163 | std::byte unknown3[8]; 164 | 165 | /** 166 | * @brief The name. 167 | * 168 | * @par Offset 169 | * @p 0x20 170 | */ 171 | char* name; 172 | }; 173 | ``` 174 | 175 | The base address of all structures is `0x0069F2B0`. 176 | 177 | - **Planted plants in a level** 178 | 179 | ```c++ 180 | //! The planted plant in a level. 181 | struct PlantedPlant 182 | { 183 | std::byte unknown1[321]; 184 | 185 | /** 186 | * @brief If the plant is invalid. 187 | * 188 | * @details If this field is @p true, the plant will be removed. 189 | * 190 | * @par Offset 191 | * @p 0x141 192 | */ 193 | bool invalid; 194 | 195 | std::byte unknown2[10]; 196 | }; 197 | ``` 198 | 199 | ## Functions 200 | 201 | ### Game Running 202 | 203 | - **Start a level.** 204 | 205 | ```c++ 206 | /** 207 | * @brief Start a level. 208 | * 209 | * @param level_id A level ID. 210 | * @param reset It's @p true when starting a new level, @p false when loading the last progress. 211 | * 212 | * @par Binary Address 213 | * @p 0x0044F560 214 | */ 215 | void StartLevel( 216 | std::int32_t level_id, 217 | bool reset); 218 | ``` 219 | 220 | - **Terminate the level.** 221 | 222 | ```c++ 223 | /** 224 | * @brief Terminate the level. 225 | * 226 | * @param level_env The level environment. 227 | * @param unknown It's always @p 0. 228 | * 229 | * @par Binary Address 230 | * @p 0x00413400 231 | */ 232 | void EndLevel( 233 | void* level_env, 234 | std::int32_t unknown); 235 | ``` 236 | 237 | - **Subtract the amount of sun.** 238 | 239 | ```c++ 240 | /** 241 | * @brief Subtract the amount of sun. 242 | * 243 | * @param count The amount to be subtracted, stored in @p EBX. 244 | * @return @p true if the amount of sun is enough, otherwise @p false, stored in @p AL. 245 | * 246 | * @par Binary Address 247 | * @p 0x0041BA60 248 | */ 249 | bool SubtractSun( 250 | std::int32_t count); 251 | ``` 252 | 253 | - **Disable automatic pause.** 254 | 255 | ```assembly 256 | .text:0044F475 mov ecx, esi 257 | .text:0044F477 pop esi 258 | .text:0044F478 jmp sub_4502C0 259 | ``` 260 | 261 | Change `jmp sub_4502C0` to `ret`. 262 | 263 | - **Disable the running menu.** 264 | 265 | ```assembly 266 | .text:004500FC cmp eax, esi 267 | .text:004500FE mov [esp + 18h], esi 268 | .text:00450102 jz short loc_450112 269 | ``` 270 | 271 | Change `jz short loc_450112` to ` jmp 0045016Ah`. 272 | 273 | - **Load the last level.** 274 | 275 | ```assembly 276 | .text:004336E6 mov esi, ecx 277 | .text:004336E8 jnz short loc_433766 278 | ``` 279 | 280 | Change `jnz short loc_433766` to `nop`. 281 | 282 | - **Be able to run multiple game processes simultaneously.** 283 | 284 | ```assembly 285 | .text:00553F10 call ds:GetLastError 286 | .text:00553F16 cmp eax, ERROR_ALREADY_EXISTS 287 | .text:00553F1B jnz short loc_553F29 288 | ``` 289 | 290 | Change `jnz` to `jmp`. 291 | 292 | ### Plants 293 | 294 | - **Check if a plant can be planted in a certain location.** 295 | 296 | ```c++ 297 | /** 298 | * @brief Check if a plant can be planted in a certain location. 299 | * 300 | * @param level_env The level environment. 301 | * @param pos_x The X coordinate of the target location. 302 | * @param pos_y The Y coordinate of the target location, stored in @p EAX. 303 | * @param plant_id A plant ID. 304 | * @return @p 0 if the plant can be planted, otherwise the error code, stored in @p EAX. 305 | * 306 | * @par Binary Address 307 | * @p 0x0040E020 308 | */ 309 | std::int32_t CanBePlanted( 310 | void* level_env, 311 | std::int32_t pos_x, 312 | std::int32_t pos_y, 313 | std::int32_t plant_id); 314 | ``` 315 | 316 | - **Create a plant.** 317 | 318 | ```c++ 319 | /** 320 | * @brief Create a plant. 321 | * 322 | * @param level_env The level environment. 323 | * @param pos_x The X coordinate of the target location. 324 | * @param pos_y The Y coordinate of the target location, stored in @p EAX. 325 | * @param plant_id A plant ID. 326 | * @param unknown It's always @p -1. 327 | * @return Unknown. 328 | * 329 | * @par Binary Address 330 | * @p 0x0040D120 331 | */ 332 | std::int32_t CreatePlant( 333 | void* level_env, 334 | std::int32_t pos_x, 335 | std::int32_t pos_y, 336 | std::int32_t plant_id, 337 | std::int32_t unknown); 338 | ``` 339 | 340 | - **Remove a plant.** 341 | 342 | ```c++ 343 | /** 344 | * @brief Remove a plant. 345 | * 346 | * @param plant A plant. 347 | * 348 | * @par Binary Address 349 | * @p 0x004679B0 350 | */ 351 | void RemovePlant( 352 | void* plant); 353 | ``` 354 | 355 | - **Remove all plants.** 356 | 357 | ```assembly 358 | .text:0041BB28 cmp [eax + 141h], bl 359 | .text:0041BB2E jz short loc_41BAE0 360 | ``` 361 | 362 | Change `jz short loc_41BAE0` to `nop`. 363 | 364 | ### Zombies 365 | 366 | - **Create a zombie.** 367 | 368 | ```c++ 369 | /** 370 | * @brief Create a zombie. 371 | * 372 | * @param level_env The level environment, stored in @p ECX. 373 | * @param zombie_id A zombie ID. 374 | * @param pos_x The X coordinate of the target location. 375 | * @param pos_y The X coordinate of the target location, stored in @p EAX. 376 | * @return Unknown. 377 | * 378 | * @par Binary Address 379 | * @p 0x0042A0F0 380 | */ 381 | std::int32_t CreateZombie( 382 | void* level_env, 383 | std::int32_t zombie_id, 384 | std::int32_t pos_x, 385 | std::int32_t pos_y); 386 | ``` 387 | 388 | - **Stop generating zombies.** 389 | 390 | ```assembly 391 | .text:0040DDD5 cmp [esi + 0A0h], eax 392 | .text:0040DDDB push edi 393 | .text:0040DDDC jb short loc_40DDE8 394 | ``` 395 | 396 | Change `jb short loc_40DDE8` to `nop`. 397 | 398 | - **Make zombies walk backwards.** 399 | 400 | ```assembly 401 | .text:0052AB21 cmp dword ptr [esi + 28h], 28h 402 | .text:0052AB25 jz short loc_52AB30 403 | ``` 404 | 405 | Change `jz` to `jmp`. 406 | 407 | - **Kill all zombies.** 408 | 409 | ```assembly 410 | .text:0052AB3E jnz loc_52ABE8 411 | .text:0052AB44 cmp dword ptr [esi + 6Ch], 0FFFFFFFCh 412 | ``` 413 | 414 | Change these two instructions to `mov dword ptr [esi + 28h], 3`. -------------------------------------------------------------------------------- /docs/badges/C++.svg: -------------------------------------------------------------------------------- 1 | C++C++ -------------------------------------------------------------------------------- /docs/badges/License-MIT.svg: -------------------------------------------------------------------------------- 1 | License: MITLicenseMIT -------------------------------------------------------------------------------- /docs/badges/MASM.svg: -------------------------------------------------------------------------------- 1 | MASMMASM -------------------------------------------------------------------------------- /docs/badges/Made-with-CMake.svg: -------------------------------------------------------------------------------- 1 | Made with: CMakeMade withCMake -------------------------------------------------------------------------------- /docs/badges/Made-with-Visual-Studio.svg: -------------------------------------------------------------------------------- 1 | Made with: Visual StudioMade withVisual Studio -------------------------------------------------------------------------------- /docs/badges/Microsoft-Windows.svg: -------------------------------------------------------------------------------- 1 | Microsoft: WindowsMicrosoftWindows -------------------------------------------------------------------------------- /docs/images/continue-or-new-game.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zhuagenborn/Plants-vs.-Zombies-Online-Battle/42a2cc613560f31a28559ed85e8d807201dd8121/docs/images/continue-or-new-game.png -------------------------------------------------------------------------------- /docs/images/i-zombie-endless.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zhuagenborn/Plants-vs.-Zombies-Online-Battle/42a2cc613560f31a28559ed85e8d807201dd8121/docs/images/i-zombie-endless.png -------------------------------------------------------------------------------- /docs/images/online-battle.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zhuagenborn/Plants-vs.-Zombies-Online-Battle/42a2cc613560f31a28559ed85e8d807201dd8121/docs/images/online-battle.gif -------------------------------------------------------------------------------- /docs/images/puzzle-mode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zhuagenborn/Plants-vs.-Zombies-Online-Battle/42a2cc613560f31a28559ed85e8d807201dd8121/docs/images/puzzle-mode.png -------------------------------------------------------------------------------- /game/Fix 'Fatal Error' on Windows 10.md: -------------------------------------------------------------------------------- 1 | # Fix "Fatal Error" on Windows 10 2 | 3 | 1. Click the *Start* button, then select "*Settings*" -> "*Time & Language*" -> "*Language*". 4 | 2. Under "*Preferred languages*", select "*English (United States)*", and then select "*Options*". you may have to add English language to get additional options. 5 | 3. Select "*Add a keyboard*" and choose "*US*". 6 | 4. Switch the current keyboard layout to "*US keyboard*". 7 | 5. Run the game. -------------------------------------------------------------------------------- /game/Plants vs. Zombies Chinese Version.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zhuagenborn/Plants-vs.-Zombies-Online-Battle/42a2cc613560f31a28559ed85e8d807201dd8121/game/Plants vs. Zombies Chinese Version.zip -------------------------------------------------------------------------------- /game/userdata/user1.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zhuagenborn/Plants-vs.-Zombies-Online-Battle/42a2cc613560f31a28559ed85e8d807201dd8121/game/userdata/user1.dat -------------------------------------------------------------------------------- /game/userdata/users.dat: -------------------------------------------------------------------------------- 1 | Test -------------------------------------------------------------------------------- /include/game/config.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file config.h 3 | * @brief Configurations. 4 | * 5 | * @author Chen Zhenshuo (chenzs108@outlook.com) 6 | * @author Liu Guowen (liu.guowen@outlook.com) 7 | * @version 1.0 8 | * @date 2021-07-10 9 | * @par GitHub 10 | * https://github.com/czs108 11 | * @par 12 | * https://github.com/lgw1995 13 | */ 14 | 15 | #pragma once 16 | 17 | #include "network/ip_addr.h" 18 | 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | 26 | 27 | namespace game { 28 | 29 | //! Players' roles. 30 | enum class Role { Plant, Zombie }; 31 | 32 | //! The number of slots. 33 | inline constexpr std::size_t slot_num{ 9 }; 34 | 35 | 36 | namespace cfg { 37 | 38 | //! Slots. 39 | using Slots = std::array; 40 | 41 | //! Player-related configurations. 42 | class Player final { 43 | public: 44 | //! The default zombie slots. 45 | static const Slots default_zombies; 46 | 47 | //! The default plant slots. 48 | static const Slots default_plants; 49 | 50 | //! Get zombie slots. 51 | const Slots& ZombieSlots() const noexcept; 52 | 53 | //! Get plant slots. 54 | const Slots& PlantSlots() const noexcept; 55 | 56 | private: 57 | Slots zombies_{ default_zombies }; 58 | Slots plants_{ default_plants }; 59 | }; 60 | 61 | //! Build IPv4 or IPv6. 62 | #ifdef INET6 63 | using IpAddr = net::Ipv6Addr; 64 | #else 65 | using IpAddr = net::Ipv4Addr; 66 | #endif // INET6 67 | 68 | //! Network-related configurations. 69 | class Network final { 70 | public: 71 | //! The default server IP address. 72 | #ifdef INET6 73 | static constexpr std::string_view default_server_ip{ 74 | net::Ipv6Addr::loop_back 75 | }; 76 | #else 77 | static constexpr std::string_view default_server_ip{ 78 | net::Ipv4Addr::loop_back 79 | }; 80 | #endif // INET6 81 | 82 | //! The default port number. 83 | static constexpr std::uint16_t default_port{ 10000 }; 84 | 85 | Network() noexcept; 86 | 87 | /** 88 | * @brief Load configurations from an @p .ini file. 89 | * 90 | * @param file A file path. 91 | */ 92 | Network(std::string_view file) noexcept; 93 | 94 | //! Get the server IP address. 95 | std::string_view ServerIp() const noexcept; 96 | 97 | //! Get the port number. 98 | std::uint16_t Port() const noexcept; 99 | 100 | private: 101 | //! The section name of network configurations in the @p .ini file. 102 | static constexpr std::string_view ini_section{ "Network" }; 103 | 104 | //! The key name of the server IP address in the @p .ini file. 105 | static constexpr std::string_view ip_ini_key{ "ServerIP" }; 106 | 107 | //! The key name of the port number in the @p .ini file. 108 | static constexpr std::string_view port_ini_key{ "Port" }; 109 | 110 | std::string server_ip_{ default_server_ip }; 111 | std::uint16_t port_{ default_port }; 112 | }; 113 | 114 | } // namespace cfg 115 | 116 | //! Configurations. 117 | class Config final { 118 | public: 119 | Config() noexcept; 120 | 121 | /** 122 | * @brief Load configurations from an @p .ini file. 123 | * 124 | * @param file A file path. 125 | */ 126 | Config(std::string_view file) noexcept; 127 | 128 | //! Get player-related configurations. 129 | const cfg::Player& Player() const noexcept; 130 | 131 | //! Get network-related configurations. 132 | const cfg::Network& Network() const noexcept; 133 | 134 | private: 135 | cfg::Player player_; 136 | cfg::Network network_; 137 | }; 138 | 139 | } // namespace game -------------------------------------------------------------------------------- /include/game/startup.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file startup.h 3 | * @brief The initializer. 4 | * 5 | * @author Chen Zhenshuo (chenzs108@outlook.com) 6 | * @author Liu Guowen (liu.guowen@outlook.com) 7 | * @version 1.0 8 | * @date 2021-07-10 9 | * @par GitHub 10 | * https://github.com/czs108 11 | * @par 12 | * https://github.com/lgw1995 13 | */ 14 | 15 | #pragma once 16 | 17 | #include "config.h" 18 | 19 | 20 | namespace game { 21 | 22 | //! The initializer. 23 | class Startup final { 24 | public: 25 | /** 26 | * @brief Construct an initializer. 27 | * 28 | * @param role A player's role. 29 | * @param cfg A configuration. 30 | */ 31 | Startup(Role role, Config cfg) noexcept; 32 | 33 | ~Startup() noexcept; 34 | 35 | //! Initialize the game. 36 | void Run(); 37 | 38 | //! Release resources. 39 | void Stop() noexcept; 40 | }; 41 | 42 | } // namespace game 43 | -------------------------------------------------------------------------------- /include/network/ip_addr.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file ip_addr.h 3 | * @brief IPv4 and IPv6 address. 4 | * 5 | * @author Chen Zhenshuo (chenzs108@outlook.com) 6 | * @author Liu Guowen (liu.guowen@outlook.com) 7 | * @version 1.0 8 | * @date 2021-08-01 9 | * @par GitHub 10 | * https://github.com/czs108 11 | * @par 12 | * https://github.com/lgw1995 13 | */ 14 | 15 | #pragma once 16 | 17 | #define _WINSOCKAPI_ 18 | 19 | #include 20 | #include 21 | 22 | #include 23 | #include 24 | #include 25 | 26 | namespace net { 27 | 28 | //! The interface of IP address. 29 | class IpAddr { 30 | public: 31 | virtual ~IpAddr() noexcept = default; 32 | 33 | //! Get the IP version. 34 | virtual int Version() const noexcept = 0; 35 | 36 | //! Get the size of socket address. 37 | virtual std::size_t Size() const noexcept = 0; 38 | 39 | //! Get the socket address. 40 | virtual const sockaddr* Raw() const noexcept = 0; 41 | }; 42 | 43 | //! The IPv4 address. 44 | class Ipv4Addr final : public IpAddr { 45 | public: 46 | static constexpr int version{ AF_INET }; 47 | 48 | static constexpr std::string_view loop_back{ "127.0.0.1" }; 49 | 50 | static constexpr std::string_view any{ "0.0.0.0" }; 51 | 52 | using RawType = sockaddr_in; 53 | 54 | explicit Ipv4Addr(const sockaddr_in& addr) noexcept; 55 | 56 | explicit Ipv4Addr(std::string_view ip, std::uint16_t port); 57 | 58 | int Version() const noexcept override; 59 | 60 | std::size_t Size() const noexcept override; 61 | 62 | const sockaddr* Raw() const noexcept override; 63 | 64 | private: 65 | sockaddr_in addr_{}; 66 | }; 67 | 68 | //! The IPv6 address. 69 | class Ipv6Addr final : public IpAddr { 70 | public: 71 | static constexpr int version{ AF_INET6 }; 72 | 73 | static constexpr std::string_view loop_back{ "::1" }; 74 | 75 | static constexpr std::string_view any{ "::" }; 76 | 77 | using RawType = sockaddr_in6; 78 | 79 | explicit Ipv6Addr(const sockaddr_in6& addr) noexcept; 80 | 81 | explicit Ipv6Addr(std::string_view ip, std::uint16_t port); 82 | 83 | int Version() const noexcept override; 84 | 85 | std::size_t Size() const noexcept override; 86 | 87 | const sockaddr* Raw() const noexcept override; 88 | 89 | private: 90 | sockaddr_in6 addr_{}; 91 | }; 92 | 93 | template 94 | concept ValidIpAddr = 95 | std::derived_from && !std::same_as && requires(T) { 96 | { T::version } -> std::convertible_to; 97 | typename T::RawType; 98 | }; 99 | 100 | } // namespace net -------------------------------------------------------------------------------- /include/network/listener.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file listener.h 3 | * @brief The TCP server. 4 | * 5 | * @author Chen Zhenshuo (chenzs108@outlook.com) 6 | * @author Liu Guowen (liu.guowen@outlook.com) 7 | * @version 1.0 8 | * @date 2021-07-10 9 | * @par GitHub 10 | * https://github.com/czs108 11 | * @par 12 | * https://github.com/lgw1995 13 | */ 14 | 15 | #pragma once 16 | 17 | #define _WINSOCKAPI_ 18 | 19 | #include "ip_addr.h" 20 | #include "socket/tcp.h" 21 | 22 | #include "system/windows_error.h" 23 | 24 | #include 25 | 26 | 27 | namespace net { 28 | 29 | /** 30 | * @brief The TCP server. 31 | * 32 | * @tparam ADDR An IP address. 33 | */ 34 | template 35 | class Listener final { 36 | public: 37 | /** 38 | * @brief Bind an IP address to the server. 39 | * 40 | * @param addr An IP address. 41 | * 42 | * @exception std::system_error The operation failed. 43 | */ 44 | void Bind(const ADDR& addr); 45 | 46 | /** 47 | * @brief Start to listen. 48 | * 49 | * @exception std::system_error The operation failed. 50 | */ 51 | void Listen(); 52 | 53 | //! Close the server. 54 | void Close() noexcept; 55 | 56 | /** 57 | * @brief Accept a connection. 58 | * 59 | * @exception std::system_error The operation failed. 60 | */ 61 | TcpSocket Accept(); 62 | 63 | private: 64 | TcpSocket socket_{}; 65 | }; 66 | 67 | 68 | template 69 | void Listener::Bind(const ADDR& addr) { 70 | socket_.SetAddr(addr); 71 | socket_.Bind(); 72 | } 73 | 74 | 75 | template 76 | void Listener::Listen() { 77 | if (listen(socket_.ID(), SOMAXCONN) == SOCKET_ERROR) { 78 | sys::ThrowWsaLastError(); 79 | } 80 | } 81 | 82 | 83 | template 84 | void Listener::Close() noexcept { 85 | socket_.Close(); 86 | } 87 | 88 | template 89 | TcpSocket Listener::Accept() { 90 | typename ADDR::RawType addr{}; 91 | int size{ sizeof(addr) }; 92 | 93 | if (const auto new_id{ 94 | accept(socket_.ID(), reinterpret_cast(&addr), &size) }; 95 | new_id != INVALID_SOCKET) { 96 | return { new_id }; 97 | } else { 98 | sys::ThrowWsaLastError(); 99 | } 100 | } 101 | 102 | } // namespace net -------------------------------------------------------------------------------- /include/network/packet.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file packet.h 3 | * @brief The network packet. 4 | * 5 | * @author Chen Zhenshuo (chenzs108@outlook.com) 6 | * @author Liu Guowen (liu.guowen@outlook.com) 7 | * @version 1.0 8 | * @date 2021-07-10 9 | * @par GitHub 10 | * https://github.com/czs108 11 | * @par 12 | * https://github.com/lgw1995 13 | */ 14 | 15 | #pragma once 16 | 17 | #include "socket/tcp.h" 18 | 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | 25 | 26 | namespace net { 27 | 28 | //! The header of a network packet. 29 | struct alignas(std::int32_t) Header { 30 | //! The size of the following data. 31 | std::size_t size; 32 | }; 33 | 34 | //! The network packet. 35 | class Packet { 36 | public: 37 | /** 38 | * @brief Receive a packet 39 | * 40 | * @tparam ADDR A valid IP address. 41 | * @param socket A socket. 42 | * @return A packet. 43 | */ 44 | template 45 | static Packet Recv(const TcpSocket& socket) { 46 | Packet pkg{}; 47 | 48 | Header header{}; 49 | socket.Recv({ reinterpret_cast(&header), sizeof(header) }); 50 | pkg.Write({ reinterpret_cast(&header), sizeof(header) }); 51 | 52 | std::unique_ptr body{ new std::byte[header.size]{} }; 53 | socket.Recv({ body.get(), header.size }); 54 | pkg.Write({ body.get(), header.size }); 55 | return pkg; 56 | } 57 | 58 | /** 59 | * @brief Send the packet. 60 | * 61 | * @tparam ADDR An IP address. 62 | * @param socket A socket. 63 | */ 64 | template 65 | void Send(const TcpSocket& socket) { 66 | socket.Send(buffer_); 67 | } 68 | 69 | /** 70 | * @brief Write data. 71 | * 72 | * @param data A buffer. 73 | * @return The packet size. 74 | */ 75 | std::size_t Write(std::span data) noexcept; 76 | 77 | /** 78 | * @brief Read data. 79 | * 80 | * @return A buffer. 81 | */ 82 | std::span Read() noexcept; 83 | 84 | private: 85 | std::vector buffer_{}; 86 | }; 87 | 88 | } // namespace net -------------------------------------------------------------------------------- /include/network/socket/basic.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file basic.h 3 | * @brief Basic socket operations. 4 | * 5 | * @author Chen Zhenshuo (chenzs108@outlook.com) 6 | * @author Liu Guowen (liu.guowen@outlook.com) 7 | * @version 1.0 8 | * @date 2021-07-10 9 | * @par GitHub 10 | * https://github.com/czs108 11 | * @par 12 | * https://github.com/lgw1995 13 | */ 14 | 15 | #pragma once 16 | 17 | #define _WINSOCKAPI_ 18 | 19 | #include "network/ip_addr.h" 20 | 21 | #include "system/windows_error.h" 22 | 23 | #include 24 | 25 | #include 26 | #include 27 | #include 28 | 29 | 30 | namespace net { 31 | 32 | /** 33 | * @brief Check if the data is too large to be sent or received. 34 | * 35 | * @param size The data size. 36 | * @param msg An error message. 37 | * 38 | * @exception std::overflow_error The data is too large. 39 | */ 40 | void CheckSizeLimit(std::size_t size, std::string_view msg); 41 | 42 | /** 43 | * @interface Socket 44 | * @brief The socket interface. 45 | * 46 | * @tparam ADDR An IP address. 47 | */ 48 | template 49 | class Socket { 50 | public: 51 | //! Construct an invalid socket. 52 | Socket() noexcept; 53 | 54 | /** 55 | * @brief Construct a socket from a low-level handle. 56 | * 57 | * @param id A socket handle. 58 | */ 59 | Socket(SOCKET id) noexcept; 60 | 61 | Socket(Socket&& that) noexcept; 62 | 63 | Socket& operator=(Socket&& that) & noexcept; 64 | 65 | Socket(const Socket&) = delete; 66 | 67 | Socket& operator=(const Socket&) = delete; 68 | 69 | //! Check if the socket is valid. 70 | bool Valid() const noexcept; 71 | 72 | //! Get the low-level socket handle. 73 | SOCKET ID() const noexcept; 74 | 75 | /** 76 | * @brief Get the IP address. 77 | * 78 | * @exception std::runtime_error The IP address is empty. 79 | */ 80 | const ADDR& Addr() const; 81 | 82 | //! Set An IP address. 83 | void SetAddr(const ADDR& addr) noexcept; 84 | 85 | /** 86 | * @brief Bind the IP address set by @p SetAddr to the socket. 87 | * 88 | * @exception std::system_error The operation failed. 89 | */ 90 | void Bind() const; 91 | 92 | //! Close the socket. 93 | void Close() noexcept; 94 | 95 | protected: 96 | virtual ~Socket() noexcept; 97 | 98 | SOCKET id_{ INVALID_SOCKET }; 99 | 100 | private: 101 | std::unique_ptr addr_{}; 102 | }; 103 | 104 | 105 | template 106 | Socket::Socket() noexcept = default; 107 | 108 | template 109 | Socket::Socket(const SOCKET id) noexcept : id_{ id } {} 110 | 111 | template 112 | Socket::Socket(Socket&& that) noexcept : 113 | id_{ that.id_ }, addr_{ std::move(that.addr_) } { 114 | that.id_ = INVALID_SOCKET; 115 | } 116 | 117 | template 118 | Socket& Socket::operator=(Socket&& that) & noexcept { 119 | id_ = that.id_; 120 | addr_ = std::move(that.addr_); 121 | that.id_ = INVALID_SOCKET; 122 | return *this; 123 | } 124 | 125 | template 126 | Socket::~Socket() noexcept { 127 | Close(); 128 | } 129 | 130 | template 131 | bool Socket::Valid() const noexcept { 132 | return id_ != INVALID_SOCKET; 133 | } 134 | 135 | template 136 | SOCKET Socket::ID() const noexcept { 137 | return id_; 138 | } 139 | 140 | template 141 | const ADDR& Socket::Addr() const { 142 | if (addr_ == nullptr) { 143 | throw std::runtime_error{ "The IP address is null." }; 144 | } 145 | 146 | return *addr_; 147 | } 148 | 149 | 150 | template 151 | void Socket::SetAddr(const ADDR& addr) noexcept { 152 | addr_ = std::make_unique(addr); 153 | } 154 | 155 | template 156 | void Socket::Bind() const { 157 | if (bind(id_, Addr().Raw(), Addr().Size()) == SOCKET_ERROR) { 158 | sys::ThrowWsaLastError(); 159 | } 160 | } 161 | 162 | 163 | template 164 | void Socket::Close() noexcept { 165 | if (Valid()) { 166 | closesocket(id_); 167 | id_ = INVALID_SOCKET; 168 | } 169 | } 170 | 171 | } // namespace net -------------------------------------------------------------------------------- /include/network/socket/tcp.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file tcp.h 3 | * @brief The TCP socket. 4 | * 5 | * @author Chen Zhenshuo (chenzs108@outlook.com) 6 | * @author Liu Guowen (liu.guowen@outlook.com) 7 | * @version 1.0 8 | * @date 2021-07-10 9 | * @par GitHub 10 | * https://github.com/czs108 11 | * @par 12 | * https://github.com/lgw1995 13 | */ 14 | 15 | #pragma once 16 | 17 | #define _WINSOCKAPI_ 18 | 19 | #include "basic.h" 20 | 21 | #include "system/windows_error.h" 22 | 23 | #include 24 | #include 25 | 26 | 27 | namespace net { 28 | 29 | /** 30 | * @brief The TCP socket. 31 | * 32 | * @tparam ADDR An IP address. 33 | */ 34 | template 35 | class TcpSocket : public Socket { 36 | public: 37 | using Socket::Socket; 38 | 39 | /** 40 | * @brief Construct a TCP socket. 41 | * 42 | * @exception std::system_error The initialization failed. 43 | */ 44 | TcpSocket(); 45 | 46 | /** 47 | * @brief Connect to a server. 48 | * 49 | * @param addr An IP address. 50 | * 51 | * @exception std::system_error The operation failed. 52 | */ 53 | void Connect(const ADDR& addr) const; 54 | 55 | /** 56 | * @brief Send data. 57 | * 58 | * @exception std::system_error The operation failed. 59 | */ 60 | std::size_t Send(std::span data) const; 61 | 62 | /** 63 | * @brief Receive data. 64 | * 65 | * @param buffer A buffer storing data. The function tries to fill this buffer. 66 | * 67 | * @exception std::system_error The operation failed. 68 | */ 69 | std::size_t Recv(std::span buffer) const; 70 | }; 71 | 72 | 73 | template 74 | TcpSocket::TcpSocket() : 75 | Socket{ socket(ADDR::version, SOCK_STREAM, 0) } { 76 | if (this->id_ == INVALID_SOCKET) { 77 | sys::ThrowWsaLastError(); 78 | } 79 | } 80 | 81 | template 82 | void TcpSocket::Connect(const ADDR& addr) const { 83 | if (connect(this->id_, addr.Raw(), addr.Size()) == SOCKET_ERROR) { 84 | sys::ThrowWsaLastError(); 85 | } 86 | } 87 | 88 | 89 | template 90 | std::size_t TcpSocket::Send(const std::span data) const { 91 | CheckSizeLimit(data.size_bytes(), "The size of data is too large to send."); 92 | 93 | if (const auto sent{ send(this->id_, 94 | reinterpret_cast(data.data()), 95 | data.size_bytes(), 0) }; 96 | sent != SOCKET_ERROR) { 97 | return static_cast(sent); 98 | } else { 99 | sys::ThrowWsaLastError(); 100 | } 101 | } 102 | 103 | template 104 | std::size_t TcpSocket::Recv(const std::span buffer) const { 105 | CheckSizeLimit(buffer.size_bytes(), 106 | "The size of buffer is too large to receive data."); 107 | 108 | if (const auto received{ recv(this->id_, 109 | reinterpret_cast(buffer.data()), 110 | buffer.size_bytes(), 0) }; 111 | received != SOCKET_ERROR) { 112 | return received; 113 | } else { 114 | sys::ThrowWsaLastError(); 115 | } 116 | } 117 | 118 | } // namespace net -------------------------------------------------------------------------------- /include/system/memory.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file memory.h 3 | * @brief Machine code and memory operations. 4 | * 5 | * @author Chen Zhenshuo (chenzs108@outlook.com) 6 | * @author Liu Guowen (liu.guowen@outlook.com) 7 | * @version 1.0 8 | * @date 2021-07-10 9 | * @par GitHub 10 | * https://github.com/czs108 11 | * @par 12 | * https://github.com/lgw1995 13 | */ 14 | 15 | #pragma once 16 | 17 | #include 18 | 19 | #include 20 | #include 21 | #include 22 | #include 23 | 24 | 25 | namespace sys { 26 | 27 | //! The operation code of long @p jmp instruction. 28 | inline constexpr std::byte jmp{ std::byte{ 0xE9 } }; 29 | 30 | //! The operation code of short @p jmp instruction. 31 | inline constexpr std::byte short_jmp{ std::byte{ 0xEB } }; 32 | 33 | //! The operation code of @p call instruction. 34 | inline constexpr std::byte call{ std::byte{ 0xE8 } }; 35 | 36 | //! The operation code of @p ret instruction. 37 | inline constexpr std::byte ret{ std::byte{ 0xC3 } }; 38 | 39 | //! The operation code of @p nop instruction. 40 | inline constexpr std::byte nop{ std::byte{ 0x90 } }; 41 | 42 | //! The length of long @p jmp instruction. 43 | inline constexpr std::size_t jmp_len{ 5 }; 44 | 45 | //! The length of @p call instruction. 46 | inline constexpr std::size_t call_len{ jmp_len }; 47 | 48 | /** 49 | * @brief Check if an address is @p nullptr. 50 | * 51 | * @param addr An address. 52 | * 53 | * @exception std::invalid_argument The address is @p nullptr. 54 | */ 55 | void CheckNullPointer(std::intptr_t addr); 56 | 57 | /** 58 | * @brief Make a part of memory writable. 59 | * 60 | * @param addr A base address. 61 | * @param size The size. 62 | * 63 | * @exception std::system_error The operation failed. 64 | */ 65 | void SetMemoryWritable(std::intptr_t addr, std::size_t size); 66 | 67 | /** 68 | * @brief Alter the memory content. 69 | * 70 | * @param addr An address. 71 | * @param new_bytes New bytes. 72 | * @param origin_bytes A buffer for storing original bytes. It's optional. 73 | * 74 | * @exception std::length_error The buffer for original bytes is too small. 75 | */ 76 | void AlterMemory(std::intptr_t addr, std::span new_bytes, 77 | std::span origin_bytes); 78 | 79 | /** 80 | * @brief Format a long @p jmp instruction. 81 | * 82 | * @param from The source address. 83 | * @param to The destination address. 84 | * @return The machine code of the instruction. 85 | */ 86 | std::array FormatJmpBytes(std::intptr_t from, 87 | std::intptr_t to) noexcept; 88 | 89 | /** 90 | * @brief Get the true entry address of a function. 91 | * 92 | * @details Some compilers will generate a Jump Thunk for each function, using @p & operator can only get the address of the thunk. 93 | */ 94 | std::intptr_t GetFuncEntryAddr(std::intptr_t addr) noexcept; 95 | 96 | } // namespace sys -------------------------------------------------------------------------------- /include/system/windows_error.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file windows_error.h 3 | * @brief Error handling. 4 | * 5 | * @author Chen Zhenshuo (chenzs108@outlook.com) 6 | * @author Liu Guowen (liu.guowen@outlook.com) 7 | * @version 1.0 8 | * @date 2021-07-10 9 | * @par GitHub 10 | * https://github.com/czs108 11 | * @par 12 | * https://github.com/lgw1995 13 | * 14 | * @see https://docs.microsoft.com/en-us/windows/win32/debug/system-error-codes 15 | */ 16 | 17 | #pragma once 18 | 19 | 20 | namespace sys { 21 | 22 | //! Throw a @p std::system_error exception containing the Windows Sockets last-error. 23 | [[noreturn]] void ThrowWsaLastError(); 24 | 25 | //! Throw a @p std::system_error exception containing the last-error. 26 | [[noreturn]] void ThrowLastError(); 27 | 28 | } // namespace sys -------------------------------------------------------------------------------- /online_config.ini: -------------------------------------------------------------------------------- 1 | [Network] 2 | ServerIP=127.0.0.1 3 | Port=10000 -------------------------------------------------------------------------------- /src/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_subdirectory(game) 2 | add_subdirectory(network) 3 | add_subdirectory(system) -------------------------------------------------------------------------------- /src/game/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_library(game) 2 | # target_compile_definitions(game PRIVATE INET6=1) 3 | 4 | set(HEADER_PATH ${PROJECT_SOURCE_DIR}/include/game) 5 | target_include_directories(game PRIVATE ${HEADER_PATH}) 6 | target_include_directories(game PRIVATE ${CMAKE_CURRENT_LIST_DIR}) 7 | target_include_directories(game PUBLIC ${PROJECT_SOURCE_DIR}/include) 8 | 9 | target_sources(game 10 | PUBLIC 11 | ${HEADER_PATH}/config.h 12 | ${HEADER_PATH}/startup.h 13 | PRIVATE 14 | state.h 15 | state.cpp 16 | startup.cpp 17 | config.cpp 18 | 19 | mod/interface.h 20 | mod/interface.cpp 21 | mod/mod.h 22 | mod/mod.cpp 23 | 24 | mod/hook/interface.h 25 | mod/hook/interface.cpp 26 | mod/hook/hook.h 27 | mod/hook/hook.cpp 28 | mod/hook/net_packet.h 29 | mod/hook/net_packet.cpp 30 | ) 31 | 32 | target_link_libraries(game PUBLIC network) 33 | target_link_libraries(game PRIVATE system) -------------------------------------------------------------------------------- /src/game/config.cpp: -------------------------------------------------------------------------------- 1 | #include "config.h" 2 | 3 | #include 4 | 5 | 6 | namespace game { 7 | 8 | namespace cfg { 9 | 10 | const Slots Player::default_zombies{ 0, 1, 2, 3, 4, 5, 6, 7, 8 }; 11 | 12 | const Slots Player::default_plants{ 0, 1, 2, 3, 4, 5, 6, 7, 8 }; 13 | 14 | const Slots& Player::ZombieSlots() const noexcept { 15 | return zombies_; 16 | } 17 | 18 | const Slots& Player::PlantSlots() const noexcept { 19 | return plants_; 20 | } 21 | 22 | 23 | Network::Network() noexcept = default; 24 | 25 | Network::Network(const std::string_view file) noexcept { 26 | char ip[256]{}; 27 | if (const auto ip_size{ GetPrivateProfileStringA(ini_section.data(), 28 | ip_ini_key.data(), "", ip, 29 | sizeof(ip), file.data()) }; 30 | ip_size != 0) { 31 | server_ip_ = ip; 32 | } 33 | 34 | port_ = GetPrivateProfileIntA(ini_section.data(), port_ini_key.data(), 35 | default_port, file.data()); 36 | } 37 | 38 | 39 | std::string_view Network::ServerIp() const noexcept { 40 | return server_ip_; 41 | } 42 | 43 | std::uint16_t Network::Port() const noexcept { 44 | return port_; 45 | } 46 | 47 | } // namespace cfg 48 | 49 | 50 | Config::Config() noexcept = default; 51 | 52 | Config::Config(const std::string_view file) noexcept : network_{ file } {} 53 | 54 | const cfg::Player& Config::Player() const noexcept { 55 | return player_; 56 | } 57 | 58 | const cfg::Network& Config::Network() const noexcept { 59 | return network_; 60 | } 61 | 62 | } // namespace game -------------------------------------------------------------------------------- /src/game/mod/hook/hook.cpp: -------------------------------------------------------------------------------- 1 | #include "hook.h" 2 | #include "mod/mod.h" 3 | #include "net_packet.h" 4 | #include "state.h" 5 | 6 | #include "system/memory.h" 7 | #include "network/listener.h" 8 | #include "network/packet.h" 9 | #include "network/socket/tcp.h" 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | 19 | namespace game::mod::hook { 20 | 21 | using namespace sys; 22 | 23 | Hook::Trampoline BeforeLoadLevel::HookBytes() const noexcept { 24 | return { .code{ call, std::byte{ 0 }, std::byte{ 0 }, std::byte{ 0 }, 25 | std::byte{ 0 }, nop }, 26 | .jmp_offset_pos{ sizeof(call) }, 27 | .jmp_inst_len{ call_len } }; 28 | } 29 | 30 | 31 | std::string_view BeforeLoadLevel::Name() const noexcept { 32 | return "Hook-Before-Load-Level"; 33 | } 34 | 35 | std::intptr_t BeforeLoadLevel::From() const noexcept { 36 | return 0x0044F560; 37 | } 38 | 39 | std::intptr_t BeforeLoadLevel::To() const noexcept { 40 | return reinterpret_cast(&Detour); 41 | } 42 | 43 | __declspec(naked) void __stdcall BeforeLoadLevel::Detour() noexcept { 44 | __asm { 45 | cmp dword ptr ds : [esp + 8], MOD_LEVEL_ID 46 | jnz _end 47 | cmp dword ptr ds : [esp + 12], 1 48 | jnz _end 49 | 50 | pushad 51 | mov eax, Callback 52 | call eax 53 | popad 54 | 55 | _end: 56 | mov eax, dword ptr fs : [0] 57 | ret 58 | } 59 | } 60 | 61 | 62 | void __stdcall BeforeLoadLevel::Callback() noexcept { 63 | netpkg::StopRecvLoop(true); 64 | 65 | Loader loader{}; 66 | loader.Add(std::make_unique()) 67 | .Add(std::make_unique()) 68 | .Add(std::make_unique()) 69 | .Add(std::make_unique()); 70 | 71 | if (state::role == Role::Plant) { 72 | loader.Add(std::make_unique()); 73 | } 74 | 75 | try { 76 | loader.Load(); 77 | 78 | if (state::role == Role::Plant) { 79 | net::Listener listener{}; 80 | 81 | std::string_view ip{}; 82 | if (typeid(cfg::IpAddr) == typeid(net::Ipv4Addr)) { 83 | ip = net::Ipv4Addr::any; 84 | } else if (typeid(cfg::IpAddr) == typeid(net::Ipv6Addr)) { 85 | ip = net::Ipv6Addr::any; 86 | } else { 87 | assert(false); 88 | std::abort(); 89 | } 90 | 91 | listener.Bind(cfg::IpAddr{ ip, state::cfg.Network().Port() }); 92 | listener.Listen(); 93 | state::conn = std::make_unique>( 94 | listener.Accept()); 95 | 96 | } else if (state::role == Role::Zombie) { 97 | state::conn = std::make_unique>(); 98 | state::conn->Connect(cfg::IpAddr{ state::cfg.Network().ServerIp(), 99 | state::cfg.Network().Port() }); 100 | } else { 101 | assert(false); 102 | std::abort(); 103 | } 104 | 105 | } catch (const std::exception& err) { 106 | state::conn.reset(); 107 | const auto msg{ std::format("Failed to start an online battle: {}", 108 | err.what()) }; 109 | OutputDebugStringA(msg.c_str()); 110 | MessageBoxA(nullptr, msg.c_str(), "Online Battle", MB_ICONERROR); 111 | return; 112 | } 113 | 114 | state::recv_thread.stop_src = std::make_unique(); 115 | state::recv_thread.thread = std::make_unique( 116 | netpkg::RecvLoop, state::recv_thread.stop_src->get_token()); 117 | } 118 | 119 | 120 | Hook::Trampoline AfterLoadLevel::HookBytes() const noexcept { 121 | return { .code{ jmp, std::byte{ 0 }, std::byte{ 0 }, std::byte{ 0 }, 122 | std::byte{ 0 }, nop }, 123 | .jmp_offset_pos{ sizeof(jmp) }, 124 | .jmp_inst_len{ jmp_len } }; 125 | } 126 | 127 | 128 | std::string_view AfterLoadLevel::Name() const noexcept { 129 | return "Hook-After-Load-Level"; 130 | } 131 | 132 | std::intptr_t AfterLoadLevel::From() const noexcept { 133 | return 0x0042F7BC; 134 | } 135 | 136 | std::intptr_t AfterLoadLevel::To() const noexcept { 137 | return reinterpret_cast(Detour); 138 | } 139 | 140 | std::optional AfterLoadLevel::JumpRet() const noexcept { 141 | return GetFuncEntryAddr(To()) + detour_len - jmp_len; 142 | } 143 | 144 | void __stdcall AfterLoadLevel::Callback() noexcept { 145 | try { 146 | Loader{} 147 | .Add(std::make_unique(10000)) 148 | .Add(std::make_unique()) 149 | .Add(std::make_unique()) 150 | .Load(); 151 | } catch (const std::exception& err) { 152 | netpkg::StopRecvLoop(true); 153 | const auto msg{ std::format("Failed to start an online battle: {}", 154 | err.what()) }; 155 | OutputDebugStringA(msg.c_str()); 156 | MessageBoxA(nullptr, msg.c_str(), "Online Battle", MB_ICONERROR); 157 | } 158 | } 159 | 160 | 161 | __declspec(naked) void __stdcall AfterLoadLevel::Detour() noexcept { 162 | __asm { 163 | cmp dword ptr ds : [esp - 8], 0x46 164 | jnz _end 165 | cmp dword ptr ds : [esp - 4], 1 166 | jnz _end 167 | 168 | pushad 169 | mov eax, Callback 170 | call eax 171 | popad 172 | 173 | _end: 174 | lea eax, [ebp - 0x12C] 175 | // jmp 176 | nop 177 | nop 178 | nop 179 | nop 180 | nop 181 | } 182 | } 183 | 184 | 185 | Hook::Trampoline DisableRuntimeMenu::HookBytes() const noexcept { 186 | return { .code{ jmp, std::byte{ 0 }, std::byte{ 0 }, std::byte{ 0 }, 187 | std::byte{ 0 }, nop }, 188 | .jmp_offset_pos{ sizeof(jmp) }, 189 | .jmp_inst_len{ jmp_len } }; 190 | } 191 | 192 | 193 | std::string_view DisableRuntimeMenu::Name() const noexcept { 194 | return "Disable-Runtime-Menu"; 195 | } 196 | 197 | std::intptr_t DisableRuntimeMenu::From() const noexcept { 198 | return 0x00450102; 199 | } 200 | 201 | std::intptr_t DisableRuntimeMenu::To() const noexcept { 202 | return 0x0045016A; 203 | } 204 | 205 | 206 | bool InitSlots::initialized_{ false }; 207 | 208 | InitSlots::InitSlots() noexcept { 209 | initialized_ = false; 210 | } 211 | 212 | Hook::Trampoline InitSlots::HookBytes() const noexcept { 213 | return { .code{ jmp, std::byte{ 0 }, std::byte{ 0 }, std::byte{ 0 }, 214 | std::byte{ 0 }, nop, nop }, 215 | .jmp_offset_pos{ sizeof(jmp) }, 216 | .jmp_inst_len{ jmp_len } }; 217 | } 218 | 219 | 220 | std::string_view InitSlots::Name() const noexcept { 221 | return "Hook-Initialize-Slots"; 222 | } 223 | 224 | std::intptr_t InitSlots::From() const noexcept { 225 | return 0x00488220; 226 | } 227 | 228 | std::intptr_t InitSlots::To() const noexcept { 229 | return reinterpret_cast(Detour); 230 | } 231 | 232 | std::optional InitSlots::JumpRet() const noexcept { 233 | return GetFuncEntryAddr(To()) + detour_len - jmp_len; 234 | } 235 | 236 | __declspec(naked) void __stdcall InitSlots::Detour() noexcept { 237 | __asm { 238 | pushad 239 | push dword ptr ss : [esp + 32 + 4] 240 | mov edx, SetSlot 241 | call edx 242 | popad 243 | push -1 244 | push 0x0064EA08 245 | // jmp 246 | nop 247 | nop 248 | nop 249 | nop 250 | nop 251 | } 252 | } 253 | 254 | 255 | void __stdcall InitSlots::SetSlot(Slot& slot) noexcept { 256 | if (initialized_) { 257 | return; 258 | } 259 | 260 | static std::int32_t i{ 0 }; 261 | if (state::role == Role::Plant) { 262 | slot.id = state::cfg.Player().PlantSlots()[i]; 263 | } else if (state::role == Role::Zombie) { 264 | slot.id = state::cfg.Player().ZombieSlots()[i] + zombie_id_base; 265 | } else { 266 | assert(false); 267 | std::abort(); 268 | } 269 | 270 | if (++i == slot_num) { 271 | initialized_ = true; 272 | i = 0; 273 | } 274 | } 275 | 276 | 277 | Hook::Trampoline LevelEnd::HookBytes() const noexcept { 278 | return { .code{ jmp, std::byte{ 0 }, std::byte{ 0 }, std::byte{ 0 }, 279 | std::byte{ 0 }, nop }, 280 | .jmp_offset_pos{ sizeof(jmp) }, 281 | .jmp_inst_len{ jmp_len } }; 282 | } 283 | 284 | 285 | std::string_view LevelEnd::Name() const noexcept { 286 | return "Hook-Level-End"; 287 | } 288 | 289 | std::intptr_t LevelEnd::From() const noexcept { 290 | return FUNC_END_LEVEL_ADDR; 291 | } 292 | 293 | std::intptr_t LevelEnd::To() const noexcept { 294 | return reinterpret_cast(Detour); 295 | } 296 | 297 | std::optional LevelEnd::JumpRet() const noexcept { 298 | return GetFuncEntryAddr(To()) + detour_len - jmp_len; 299 | } 300 | 301 | void __stdcall LevelEnd::Callback() noexcept { 302 | if (state::conn == nullptr || !state::conn->Valid()) { 303 | return; 304 | } 305 | 306 | netpkg::Header lvl_end{}; 307 | lvl_end.size = sizeof(lvl_end); 308 | lvl_end.pkt_type = netpkg::Type::LevelEnd; 309 | lvl_end.role = state::role; 310 | 311 | try { 312 | net::Packet pkg{}; 313 | pkg.Write({ reinterpret_cast(&lvl_end), sizeof(lvl_end) }); 314 | pkg.Send(*state::conn); 315 | 316 | netpkg::StopRecvLoop(true); 317 | 318 | } catch (const std::exception& err) { 319 | const auto msg{ std::format("Failed to send a packet: {}", 320 | err.what()) }; 321 | OutputDebugStringA(msg.c_str()); 322 | } 323 | } 324 | 325 | 326 | __declspec(naked) void __stdcall LevelEnd::Detour() noexcept { 327 | __asm { 328 | pushad 329 | mov eax, Callback 330 | call eax 331 | popad 332 | push ebp 333 | mov ebp, esp 334 | and esp, 0xFFFFFFF8 335 | // jmp 336 | nop 337 | nop 338 | nop 339 | nop 340 | nop 341 | } 342 | } 343 | 344 | 345 | Hook::Trampoline CreateZombie::HookBytes() const noexcept { 346 | if (state::role == Role::Plant) { 347 | return { .code{ call, std::byte{ 0 }, std::byte{ 0 }, std::byte{ 0 }, 348 | std::byte{ 0 } }, 349 | .jmp_offset_pos{ sizeof(call) }, 350 | .jmp_inst_len{ call_len } }; 351 | } else if (state::role == Role::Zombie) { 352 | return { .code{ jmp, std::byte{ 0 }, std::byte{ 0 }, std::byte{ 0 }, 353 | std::byte{ 0 }, nop }, 354 | .jmp_offset_pos{ sizeof(jmp) }, 355 | .jmp_inst_len{ jmp_len } }; 356 | } else { 357 | assert(false); 358 | std::abort(); 359 | } 360 | } 361 | 362 | 363 | std::string_view CreateZombie::Name() const noexcept { 364 | return "Hook-Create-Zombie"; 365 | } 366 | 367 | std::intptr_t CreateZombie::From() const noexcept { 368 | if (state::role == Role::Plant) { 369 | return 0x0042A425; 370 | } else if (state::role == Role::Zombie) { 371 | return FUNC_CREATE_ZOMBIE_ADDR; 372 | } else { 373 | assert(false); 374 | std::abort(); 375 | } 376 | } 377 | 378 | 379 | std::intptr_t CreateZombie::To() const noexcept { 380 | if (state::role == Role::Plant) { 381 | return reinterpret_cast(PlantDetour); 382 | } else if (state::role == Role::Zombie) { 383 | return reinterpret_cast(ZombieDetour); 384 | } else { 385 | assert(false); 386 | std::abort(); 387 | } 388 | } 389 | 390 | 391 | std::optional CreateZombie::JumpRet() const noexcept { 392 | if (state::role == Role::Plant) { 393 | return std::nullopt; 394 | } else if (state::role == Role::Zombie) { 395 | return GetFuncEntryAddr(To()) + zombie_detour_len - jmp_len; 396 | } else { 397 | assert(false); 398 | std::abort(); 399 | } 400 | } 401 | 402 | 403 | void __stdcall CreateZombie::ZombieCallback(const std::int32_t pos_x, 404 | const std::int32_t pos_y, 405 | const std::int32_t id) noexcept { 406 | if (state::conn == nullptr || !state::conn->Valid()) { 407 | return; 408 | } 409 | 410 | netpkg::NewItem new_item{}; 411 | new_item.size = sizeof(new_item); 412 | new_item.pkt_type = netpkg::Type::NewZombie; 413 | new_item.role = Role::Zombie; 414 | new_item.pos_x = pos_x; 415 | new_item.pos_y = pos_y; 416 | new_item.id = id; 417 | 418 | try { 419 | net::Packet pkg{}; 420 | pkg.Write( 421 | { reinterpret_cast(&new_item), sizeof(new_item) }); 422 | pkg.Send(*state::conn); 423 | 424 | } catch (const std::exception& err) { 425 | const auto msg{ std::format("Failed to send a packet: {}", 426 | err.what()) }; 427 | OutputDebugStringA(msg.c_str()); 428 | } 429 | } 430 | 431 | 432 | __declspec(naked) void __stdcall CreateZombie::ZombieDetour() noexcept { 433 | __asm { 434 | pushad 435 | push dword ptr ss : [esp + 32 + 4] 436 | push eax 437 | push dword ptr ss : [esp + 32 + 16] 438 | mov eax, ZombieCallback 439 | call eax 440 | popad 441 | 442 | push ebx 443 | push ebp 444 | mov ebp, dword ptr ss : [esp + 0xC] 445 | // jmp 446 | nop 447 | nop 448 | nop 449 | nop 450 | nop 451 | } 452 | } 453 | 454 | 455 | __declspec(naked) void __stdcall CreateZombie::PlantDetour() noexcept { 456 | __asm { 457 | pushad 458 | mov edx, dword ptr ss : [esp + 32 + 4] 459 | mov ebx, dword ptr ss : [esp + 32 + 8] 460 | push -1 461 | push edx 462 | push ebx 463 | mov ebx, dword ptr ds : [BASE_ADDR] 464 | mov ebx, dword ptr ds : [ebx + 0x768] 465 | push ebx 466 | mov edx, FUNC_CREATE_PLANT_ADDR 467 | call edx 468 | popad 469 | retn 8 470 | } 471 | } 472 | 473 | 474 | Hook::Trampoline CreatePlant::HookBytes() const noexcept { 475 | return { .code{ jmp, std::byte{ 0 }, std::byte{ 0 }, std::byte{ 0 }, 476 | std::byte{ 0 }, nop, nop }, 477 | .jmp_offset_pos{ sizeof(jmp) }, 478 | .jmp_inst_len{ jmp_len } }; 479 | } 480 | 481 | 482 | std::string_view CreatePlant::Name() const noexcept { 483 | return "Hook-Create-Plant"; 484 | } 485 | 486 | std::intptr_t CreatePlant::From() const noexcept { 487 | return FUNC_CREATE_PLANT_ADDR; 488 | } 489 | 490 | std::intptr_t CreatePlant::To() const noexcept { 491 | return reinterpret_cast(Detour); 492 | } 493 | 494 | std::optional CreatePlant::JumpRet() const noexcept { 495 | return GetFuncEntryAddr(To()) + detour_len - jmp_len; 496 | } 497 | 498 | void __stdcall CreatePlant::Callback(const std::int32_t pos_x, 499 | const std::int32_t pos_y, 500 | const std::int32_t id) noexcept { 501 | if (state::conn == nullptr || !state::conn->Valid()) { 502 | return; 503 | } 504 | 505 | netpkg::NewItem new_item{}; 506 | new_item.size = sizeof(new_item); 507 | new_item.pkt_type = netpkg::Type::NewPlant; 508 | new_item.role = Role::Plant; 509 | new_item.pos_x = pos_x; 510 | new_item.pos_y = pos_y; 511 | new_item.id = id; 512 | 513 | try { 514 | net::Packet pkg{}; 515 | pkg.Write( 516 | { reinterpret_cast(&new_item), sizeof(new_item) }); 517 | pkg.Send(*state::conn); 518 | } catch (const std::exception& err) { 519 | const auto msg{ std::format("Failed to send a packet: {}", 520 | err.what()) }; 521 | OutputDebugStringA(msg.c_str()); 522 | } 523 | } 524 | 525 | 526 | __declspec(naked) void __stdcall CreatePlant::Detour() noexcept { 527 | __asm { 528 | pushad 529 | push dword ptr ss : [esp + 32 + 12] 530 | push eax 531 | push dword ptr ss : [esp + 32 + 16] 532 | mov eax, Callback 533 | call eax 534 | popad 535 | 536 | push ecx 537 | push ebx 538 | push ebp 539 | mov ebp, dword ptr ss : [esp + 0x10] 540 | // jmp 541 | nop 542 | nop 543 | nop 544 | nop 545 | nop 546 | } 547 | } 548 | 549 | } // namespace game::mod::hook -------------------------------------------------------------------------------- /src/game/mod/hook/hook.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file hook.h 3 | * @brief Hook procedures. 4 | * 5 | * @author Chen Zhenshuo (chenzs108@outlook.com) 6 | * @author Liu Guowen (liu.guowen@outlook.com) 7 | * @version 1.0 8 | * @date 2021-07-10 9 | * @par GitHub 10 | * https://github.com/czs108 11 | * @par 12 | * https://github.com/lgw1995 13 | */ 14 | 15 | #pragma once 16 | 17 | #include "interface.h" 18 | 19 | #include 20 | #include 21 | #include 22 | #include 23 | 24 | 25 | namespace game::mod::hook { 26 | 27 | //! The hook procedure before loading an online level. 28 | class BeforeLoadLevel : public Hook { 29 | public: 30 | std::string_view Name() const noexcept; 31 | 32 | protected: 33 | Trampoline HookBytes() const noexcept override; 34 | 35 | std::intptr_t From() const noexcept override; 36 | 37 | std::intptr_t To() const noexcept override; 38 | 39 | private: 40 | static void __stdcall Detour() noexcept; 41 | 42 | static void __stdcall Callback() noexcept; 43 | }; 44 | 45 | //! The hook procedure after loading an online level. 46 | class AfterLoadLevel : public Hook { 47 | public: 48 | std::string_view Name() const noexcept; 49 | 50 | protected: 51 | Trampoline HookBytes() const noexcept override; 52 | 53 | std::intptr_t From() const noexcept override; 54 | 55 | std::intptr_t To() const noexcept override; 56 | 57 | std::optional JumpRet() const noexcept override; 58 | 59 | private: 60 | static constexpr std::size_t detour_len{ 0x24 }; 61 | 62 | static void __stdcall Detour() noexcept; 63 | 64 | static void __stdcall Callback() noexcept; 65 | }; 66 | 67 | //! Disable the running menu. 68 | class DisableRuntimeMenu : public Hook { 69 | public: 70 | std::string_view Name() const noexcept override; 71 | 72 | protected: 73 | Trampoline HookBytes() const noexcept override; 74 | 75 | std::intptr_t From() const noexcept override; 76 | 77 | std::intptr_t To() const noexcept override; 78 | }; 79 | 80 | //! The slot initializer. 81 | class InitSlots : public Hook { 82 | public: 83 | InitSlots() noexcept; 84 | 85 | std::string_view Name() const noexcept override; 86 | 87 | protected: 88 | Trampoline HookBytes() const noexcept override; 89 | 90 | std::intptr_t From() const noexcept override; 91 | 92 | std::intptr_t To() const noexcept override; 93 | 94 | std::optional JumpRet() const noexcept override; 95 | 96 | private: 97 | static constexpr std::size_t detour_len{ 0x1A }; 98 | 99 | static void __stdcall SetSlot(Slot& slot) noexcept; 100 | 101 | static void __stdcall Detour() noexcept; 102 | 103 | static bool initialized_; 104 | }; 105 | 106 | //! The hook procedure at the end of a level. 107 | class LevelEnd : public Hook { 108 | public: 109 | std::string_view Name() const noexcept override; 110 | 111 | protected: 112 | Trampoline HookBytes() const noexcept override; 113 | 114 | std::intptr_t From() const noexcept override; 115 | 116 | std::intptr_t To() const noexcept override; 117 | 118 | std::optional JumpRet() const noexcept override; 119 | 120 | private: 121 | static constexpr std::size_t detour_len{ 0x14 }; 122 | 123 | static void __stdcall Callback() noexcept; 124 | 125 | static void __stdcall Detour() noexcept; 126 | }; 127 | 128 | //! The hook procedure when creating a zombie. 129 | class CreateZombie : public Hook { 130 | public: 131 | std::string_view Name() const noexcept; 132 | 133 | protected: 134 | Trampoline HookBytes() const noexcept override; 135 | 136 | std::intptr_t From() const noexcept override; 137 | 138 | std::intptr_t To() const noexcept override; 139 | 140 | std::optional JumpRet() const noexcept override; 141 | 142 | private: 143 | static constexpr std::size_t zombie_detour_len{ 0x20 }; 144 | 145 | static void __stdcall ZombieDetour() noexcept; 146 | 147 | static void __stdcall PlantDetour() noexcept; 148 | 149 | static void __stdcall ZombieCallback(std::int32_t pos_x, std::int32_t pos_y, 150 | std::int32_t id) noexcept; 151 | }; 152 | 153 | //! The hook procedure when creating a plant. 154 | class CreatePlant : public Hook { 155 | public: 156 | std::string_view Name() const noexcept; 157 | 158 | protected: 159 | Trampoline HookBytes() const noexcept override; 160 | 161 | std::intptr_t From() const noexcept override; 162 | 163 | std::intptr_t To() const noexcept override; 164 | 165 | std::optional JumpRet() const noexcept override; 166 | 167 | private: 168 | static constexpr std::size_t detour_len{ 0x21 }; 169 | 170 | static void __stdcall Detour() noexcept; 171 | 172 | static void __stdcall Callback(std::int32_t pos_x, std::int32_t pos_y, 173 | std::int32_t id) noexcept; 174 | }; 175 | 176 | } // namespace game::mod::hook -------------------------------------------------------------------------------- /src/game/mod/hook/interface.cpp: -------------------------------------------------------------------------------- 1 | #include "interface.h" 2 | 3 | #include "system/memory.h" 4 | 5 | #include 6 | 7 | 8 | namespace game { 9 | 10 | using namespace sys; 11 | 12 | void Hook::Enable() { 13 | const auto from{ From() }; 14 | Trampoline trampoline{ HookBytes() }; 15 | std::vector hook_bytes{ trampoline.code }; 16 | const auto offset{ To() - from - trampoline.jmp_inst_len }; 17 | 18 | std::memcpy(hook_bytes.data() + trampoline.jmp_offset_pos, &offset, 19 | sizeof(offset)); 20 | 21 | origin_bytes_.resize(hook_bytes.size()); 22 | AlterMemory(from, hook_bytes, origin_bytes_); 23 | 24 | const std::optional jump_ret{ JumpRet() }; 25 | if (jump_ret.has_value()) { 26 | const std::array ret_bytes{ FormatJmpBytes(jump_ret.value(), 27 | from + hook_bytes.size()) }; 28 | AlterMemory(jump_ret.value(), ret_bytes, {}); 29 | } 30 | } 31 | 32 | 33 | void Hook::Disable() { 34 | AlterMemory(From(), origin_bytes_, {}); 35 | } 36 | 37 | std::optional Hook::JumpRet() const noexcept { 38 | return std::nullopt; 39 | } 40 | 41 | std::span Hook::OriginBytes() const noexcept { 42 | return origin_bytes_; 43 | } 44 | 45 | } // namespace game -------------------------------------------------------------------------------- /src/game/mod/hook/interface.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file interface.h 3 | * @brief The hook procedure interface. 4 | * 5 | * @author Chen Zhenshuo (chenzs108@outlook.com) 6 | * @author Liu Guowen (liu.guowen@outlook.com) 7 | * @version 1.0 8 | * @date 2021-07-10 9 | * @par GitHub 10 | * https://github.com/czs108 11 | * @par 12 | * https://github.com/lgw1995 13 | */ 14 | 15 | #pragma once 16 | 17 | #include "mod/interface.h" 18 | 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | 25 | 26 | namespace game { 27 | 28 | /** 29 | * @brief The hook procedure interface. 30 | * 31 | * @details The framework is: Trampoline -> Detour -> Callback. 32 | */ 33 | class Hook : public Mod { 34 | public: 35 | void Enable() override; 36 | 37 | void Disable() override; 38 | 39 | protected: 40 | /** 41 | * @brief The trampoline used to jump to a detour function. 42 | * 43 | * @details 44 | * An example: 45 | * 46 | * @code {.cpp} 47 | * Trampoline{ .code{ jmp, std::byte{ 0 }, std::byte{ 0 }, std::byte{ 0 }, std::byte{ 0 } }, 48 | * .jmp_offset_pos{ sizeof(jmp) }, 49 | * .jmp_inst_len{ jmp_len } }; 50 | * @endcode 51 | */ 52 | struct Trampoline { 53 | //! The machine code for hook. 54 | std::vector code; 55 | 56 | //! The data offset of the jump offset to the destination in @p code array. 57 | std::size_t jmp_offset_pos; 58 | 59 | //! The length of the jump instruction. 60 | std::size_t jmp_inst_len; 61 | }; 62 | 63 | //! Get the origin bytes before hooking. 64 | std::span OriginBytes() const noexcept; 65 | 66 | //! Get the trampoline to a detour function. 67 | virtual Trampoline HookBytes() const noexcept = 0; 68 | 69 | //! Get the source address. 70 | virtual std::intptr_t From() const noexcept = 0; 71 | 72 | //! Get the destination address. 73 | virtual std::intptr_t To() const noexcept = 0; 74 | 75 | //! Get the return address of the callback function. It's optional. 76 | virtual std::optional JumpRet() const noexcept; 77 | 78 | private: 79 | std::vector origin_bytes_{}; 80 | }; 81 | 82 | } // namespace game -------------------------------------------------------------------------------- /src/game/mod/hook/net_packet.cpp: -------------------------------------------------------------------------------- 1 | #include "net_packet.h" 2 | #include "hook.h" 3 | #include "mod/interface.h" 4 | #include "state.h" 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | 11 | namespace game::netpkg { 12 | 13 | void Process(const Header* const packet) { 14 | assert(packet != nullptr); 15 | 16 | switch (packet->pkt_type) { 17 | case Type::NewPlant: { 18 | const NewItem* const item = static_cast(packet); 19 | mod::CreatePlant(item->pos_x, item->pos_y, item->id); 20 | break; 21 | } 22 | case Type::NewZombie: { 23 | const NewItem* const item = static_cast(packet); 24 | mod::CreateZombie(item->pos_x, item->pos_y, item->id); 25 | break; 26 | } 27 | case Type::LevelEnd: { 28 | mod::hook::LevelEnd{}.Disable(); 29 | mod::EndLevel(); 30 | break; 31 | } 32 | default: { 33 | throw std::invalid_argument{ "The type of packet is unknown." }; 34 | } 35 | } 36 | } 37 | 38 | 39 | void RecvLoop(const std::stop_token stop_token) noexcept { 40 | try { 41 | while (!stop_token.stop_requested()) { 42 | net::Packet pkg{ net::Packet::Recv(*state::conn) }; 43 | Process(reinterpret_cast(pkg.Read().data())); 44 | } 45 | } catch (const std::exception& err) { 46 | const auto msg{ std::format("Failed to receive or process a packet: {}", 47 | err.what()) }; 48 | OutputDebugStringA(msg.c_str()); 49 | } 50 | 51 | StopRecvLoop(false); 52 | } 53 | 54 | 55 | void StopRecvLoop(bool wait) noexcept { 56 | if (state::recv_thread.stop_src != nullptr) { 57 | state::recv_thread.stop_src->request_stop(); 58 | } 59 | 60 | if (state::conn != nullptr) { 61 | state::conn->Close(); 62 | } 63 | 64 | if (wait && state::recv_thread.thread != nullptr 65 | && state::recv_thread.thread->joinable()) { 66 | state::recv_thread.thread->join(); 67 | } 68 | 69 | state::recv_thread.stop_src.reset(); 70 | } 71 | 72 | } // namespace game::netpkg -------------------------------------------------------------------------------- /src/game/mod/hook/net_packet.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file net_packet.h 3 | * @brief Network packets and the receiver thread. 4 | * 5 | * @author Chen Zhenshuo (chenzs108@outlook.com) 6 | * @author Liu Guowen (liu.guowen@outlook.com) 7 | * @version 1.0 8 | * @date 2021-07-10 9 | * @par GitHub 10 | * https://github.com/czs108 11 | * @par 12 | * https://github.com/lgw1995 13 | */ 14 | 15 | #pragma once 16 | 17 | #include "config.h" 18 | 19 | #include "network/packet.h" 20 | 21 | #include 22 | #include 23 | 24 | 25 | namespace game::netpkg { 26 | 27 | //! Types of packets. 28 | enum class Type { NewPlant, NewZombie, LevelEnd }; 29 | 30 | //! The header of a packet. 31 | struct alignas(std::int32_t) Header : public net::Header { 32 | //! The type. 33 | Type pkt_type; 34 | 35 | //! The player's role. 36 | Role role; 37 | }; 38 | 39 | //! The packet storing a creation event. 40 | struct alignas(std::int32_t) NewItem : public Header { 41 | //! The X-coordinate of the target location. 42 | std::int32_t pos_x; 43 | 44 | //! The X-coordinate of the target location. 45 | std::int32_t pos_y; 46 | 47 | //! The item ID. 48 | std::int32_t id; 49 | }; 50 | 51 | /** 52 | * @brief Process packets. 53 | * 54 | * @param packet A packet. 55 | * 56 | * @exception std::invalid_argument An unknown packet type. 57 | */ 58 | void Process(const Header* packet); 59 | 60 | /** 61 | * @brief The receiver thread. 62 | * 63 | * @param stop_token A stop token that can stop the thread. 64 | */ 65 | void RecvLoop(std::stop_token stop_token) noexcept; 66 | 67 | /** 68 | * @brief Stop the receiver thread. 69 | * 70 | * @param wait Whether to wait for the thread to end. 71 | */ 72 | void StopRecvLoop(bool wait) noexcept; 73 | 74 | } // namespace game::netpkg -------------------------------------------------------------------------------- /src/game/mod/interface.cpp: -------------------------------------------------------------------------------- 1 | #include "interface.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | 10 | namespace game { 11 | 12 | Mod::~Mod() noexcept = default; 13 | 14 | void Mod::Disable(){}; 15 | 16 | 17 | namespace mod { 18 | 19 | __declspec(naked) void CreateZombie(const std::int32_t pos_x, 20 | const std::int32_t pos_y, 21 | const std::int32_t id) noexcept { 22 | __asm { 23 | pushad 24 | 25 | mov ebx, dword ptr ds : [BASE_ADDR] 26 | test ebx, ebx 27 | jz _end 28 | mov ebx, dword ptr ds : [ebx + 0x768] 29 | test ebx, ebx 30 | jz _end 31 | 32 | mov ecx, dword ptr ds : [ebx + 0x160] 33 | 34 | mov edx, [esp + 32 + 4] 35 | push edx 36 | 37 | mov eax, [esp + 32 + 16] 38 | push eax 39 | 40 | mov eax, [esp + 32 + 16] 41 | 42 | mov edx, FUNC_CREATE_ZOMBIE_ADDR 43 | call edx 44 | 45 | _end: 46 | popad 47 | ret 48 | } 49 | } 50 | 51 | __declspec(naked) void CreatePlant(const std::int32_t pos_x, 52 | const std::int32_t pos_y, 53 | const std::int32_t id) noexcept { 54 | __asm { 55 | pushad 56 | 57 | push -1 58 | 59 | mov eax, [esp + 32 + 16] 60 | push eax 61 | 62 | mov edx, [esp + 32 + 12] 63 | push edx 64 | 65 | mov ebx, dword ptr ds : [BASE_ADDR] 66 | test ebx, ebx 67 | jz _end 68 | 69 | mov ebx, dword ptr ds : [ebx + 0x768] 70 | push ebx 71 | 72 | mov eax, [esp + 32 + 24] 73 | 74 | mov edx, FUNC_CREATE_PLANT_ADDR 75 | call edx 76 | popad 77 | ret 78 | 79 | _end: 80 | add esp, 12 81 | popad 82 | ret 83 | } 84 | } 85 | 86 | __declspec(naked) void EndLevel() noexcept { 87 | __asm { 88 | pushad 89 | 90 | mov ebx, dword ptr ds : [BASE_ADDR] 91 | test ebx, ebx 92 | jz _end 93 | mov eax, dword ptr ds : [ebx + 0x768] 94 | test eax, eax 95 | jz _end 96 | 97 | push 0 98 | push eax 99 | mov edx, FUNC_END_LEVEL_ADDR 100 | call edx 101 | 102 | _end: 103 | popad 104 | ret 105 | } 106 | } 107 | 108 | 109 | Loader& Loader::Add(std::unique_ptr mod) { 110 | if (mod == nullptr) { 111 | throw std::invalid_argument{ "The module is null." }; 112 | } 113 | 114 | CheckDuplicate(*mod); 115 | mods_.push_back(std::move(mod)); 116 | return *this; 117 | } 118 | 119 | 120 | void Loader::Load() { 121 | std::ranges::for_each(mods_, [](auto& mod) { mod->Enable(); }); 122 | } 123 | 124 | void Loader::CheckDuplicate(const Mod& mod) const { 125 | auto cmp{ [&mod](const auto& curr) { 126 | assert(curr != nullptr); 127 | return mod.Name().compare(curr->Name()) == 0; 128 | } }; 129 | 130 | if (const auto it{ std::ranges::find_if(mods_, cmp) }; it != mods_.cend()) { 131 | throw std::invalid_argument{ std::format( 132 | "The module '{}' has been registered.", mod.Name()) }; 133 | } 134 | } 135 | 136 | } // namespace mod 137 | 138 | } // namespace game -------------------------------------------------------------------------------- /src/game/mod/interface.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file interface.h 3 | * @brief The modification interface. 4 | * 5 | * @author Chen Zhenshuo (chenzs108@outlook.com) 6 | * @author Liu Guowen (liu.guowen@outlook.com) 7 | * @version 1.0 8 | * @date 2021-07-10 9 | * @par GitHub 10 | * https://github.com/czs108 11 | * @par 12 | * https://github.com/lgw1995 13 | */ 14 | 15 | #pragma once 16 | 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | 23 | 24 | namespace game { 25 | 26 | //! The modification interface. 27 | class Mod { 28 | public: 29 | virtual ~Mod() noexcept; 30 | 31 | //! Enable the modification. 32 | virtual void Enable() = 0; 33 | 34 | //! Disable the modification. 35 | virtual void Disable(); 36 | 37 | //! Get the unique name of modification. 38 | virtual std::string_view Name() const noexcept = 0; 39 | }; 40 | 41 | 42 | namespace mod { 43 | 44 | //! The modification loader. 45 | class Loader final { 46 | public: 47 | /** 48 | * @brief Register a modification. 49 | * 50 | * @param mod A modification. 51 | * @return The current loader. 52 | */ 53 | Loader& Add(std::unique_ptr mod); 54 | 55 | //! Load registered modifications. 56 | void Load(); 57 | 58 | private: 59 | /** 60 | * @brief Check whether a modification has been registered twice. 61 | * 62 | * @param mod A modification. 63 | * 64 | * @exception std::invalid_argument The modification has been registered before. 65 | */ 66 | void CheckDuplicate(const Mod& mod) const; 67 | 68 | std::list> mods_{}; 69 | }; 70 | 71 | 72 | //! The base number of zombie IDs. 73 | inline constexpr std::int32_t zombie_id_base{ 0x3C }; 74 | 75 | /** 76 | * @brief The global base address. 77 | * 78 | * @warning 79 | * This value cannot be defined using @p constexpr. 80 | * When referring to it in a naked function defined using @p __declspec(naked), 81 | * a @p constexpr variable cannot be converted into a constant number as normal. 82 | */ 83 | #define BASE_ADDR 0x006A9F38 84 | 85 | /** 86 | * @brief The address of @p CreateZombie function. 87 | * 88 | * @warning 89 | * This value cannot be defined using @p constexpr. 90 | * When referring to it in a naked function defined using @p __declspec(naked), 91 | * a @p constexpr variable cannot be converted into a constant number as normal. 92 | */ 93 | #define FUNC_CREATE_ZOMBIE_ADDR 0x0042A0F0 94 | 95 | /** 96 | * @brief The address of @p CreatePlant function. 97 | * 98 | * @warning 99 | * This value cannot be defined using @p constexpr. 100 | * When referring to it in a naked function defined using @p __declspec(naked), 101 | * a @p constexpr variable cannot be converted into a constant number as normal. 102 | */ 103 | #define FUNC_CREATE_PLANT_ADDR 0x0040D120 104 | 105 | /** 106 | * @brief The address of @p EndLevel function. 107 | * 108 | * @warning 109 | * This value cannot be defined using @p constexpr. 110 | * When referring to it in a naked function defined using @p __declspec(naked), 111 | * a @p constexpr variable cannot be converted into a constant number as normal. 112 | */ 113 | #define FUNC_END_LEVEL_ADDR 0x00413400 114 | 115 | /** 116 | * @brief The ID of the online level. 117 | * 118 | * @warning 119 | * This value cannot be defined using @p constexpr. 120 | * When referring to it in a naked function defined using @p __declspec(naked), 121 | * a @p constexpr variable cannot be converted into a constant number as normal. 122 | */ 123 | #define MOD_LEVEL_ID 0x46 124 | 125 | /** 126 | * @brief The slot. 127 | * 128 | * @details 129 | * Set a breakpoint at @p 0x004897B2. 130 | * Each time the game is interrupted while loading a new level, 131 | * the 4-bit content of @p [ESP] will be the address of the slot structure that the game is displaying currently. 132 | */ 133 | struct Slot { 134 | std::byte unknown1[8]; 135 | 136 | /** 137 | * @brief The X-coordinate on the screen. 138 | * 139 | * @par Offset 140 | * @p 0x8 141 | */ 142 | std::int32_t screen_pos_x; 143 | 144 | /** 145 | * @brief The Y-coordinate on the screen. 146 | * 147 | * @par Offset 148 | * @p 0xC 149 | */ 150 | std::int32_t screen_pos_y; 151 | 152 | std::byte unknown2[36]; 153 | 154 | /** 155 | * @brief The item ID. 156 | * 157 | * @details 158 | * If the type of the slot is zombie, this field is equal to the zombie ID plus @p zombie_id_base. 159 | * If the type of the slot is plant, this field is equal to the plant ID. 160 | * 161 | * @par Offset 162 | * @p 0x34 163 | */ 164 | std::int32_t id; 165 | }; 166 | 167 | //! The planted plant in a level. 168 | struct PlantedPlant { 169 | std::byte unknown1[321]; 170 | 171 | /** 172 | * @brief If the plant is invalid. 173 | * 174 | * @details If this field is @p true, the plant will be removed. 175 | * 176 | * @par Offset 177 | * @p 0x141 178 | */ 179 | bool invalid; 180 | 181 | std::byte unknown2[10]; 182 | }; 183 | 184 | /** 185 | * @brief Static plant information. 186 | * 187 | * @par Binary Address 188 | * @p 0x0069F2B0 189 | */ 190 | struct Plant { 191 | /** 192 | * @brief The ID. 193 | * 194 | * @par Offset 195 | * @p 0x0 196 | */ 197 | std::int32_t id; 198 | 199 | std::byte unknown1[4]; 200 | 201 | /** 202 | * @brief The image resource. 203 | * 204 | * @par Offset 205 | * @p 0x8 206 | */ 207 | std::int32_t ui; 208 | 209 | std::byte unknown2[4]; 210 | 211 | /** 212 | * @brief The cost to plant. 213 | * 214 | * @par Offset 215 | * @p 0x10 216 | */ 217 | std::int32_t sun_cost; 218 | 219 | /** 220 | * @brief The recharge time. 221 | * 222 | * @par Offset 223 | * @p 0x14 224 | */ 225 | std::int32_t recharge_time; 226 | 227 | std::byte unknown3[8]; 228 | 229 | /** 230 | * @brief The name. 231 | * 232 | * @par Offset 233 | * @p 0x20 234 | */ 235 | char* name; 236 | }; 237 | 238 | /** 239 | * @brief Create a zombie. 240 | * 241 | * @param pos_x The X-coordinate of the target location. 242 | * @param pos_y The Y-coordinate of the target location. 243 | * @param id A zombie ID. 244 | * 245 | * @bug This function sometimes raises an access exception. 246 | */ 247 | void CreateZombie(std::int32_t pos_x, std::int32_t pos_y, 248 | std::int32_t id) noexcept; 249 | 250 | /** 251 | * @brief Create a plant. 252 | * 253 | * @param pos_x The X-coordinate of the target location. 254 | * @param pos_y The Y-coordinate of the target location. 255 | * @param id A plant ID. 256 | */ 257 | void CreatePlant(std::int32_t pos_x, std::int32_t pos_y, 258 | std::int32_t id) noexcept; 259 | 260 | //! Terminate the current level. 261 | void EndLevel() noexcept; 262 | 263 | } // namespace mod 264 | 265 | } // namespace game -------------------------------------------------------------------------------- /src/game/mod/mod.cpp: -------------------------------------------------------------------------------- 1 | #include "mod.h" 2 | 3 | #include "system/memory.h" 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | 12 | namespace game::mod { 13 | 14 | using namespace sys; 15 | 16 | SetSunAmount::SetSunAmount(const std::size_t amount) noexcept : 17 | amount_{ amount } {} 18 | 19 | std::string_view SetSunAmount::Name() const noexcept { 20 | return "Set-Sun-Amount"; 21 | } 22 | 23 | void SetSunAmount::Enable() { 24 | const auto addr1{ *reinterpret_cast(BASE_ADDR) }; 25 | if (addr1 == 0) { 26 | return; 27 | } 28 | 29 | const auto addr2{ *reinterpret_cast(addr1 + 0x768) }; 30 | if (addr2 == 0) { 31 | return; 32 | } 33 | 34 | *reinterpret_cast(addr2 + 0x5560) = amount_; 35 | } 36 | 37 | 38 | std::string_view DisableAutoPause::Name() const noexcept { 39 | return "Disable-Auto-Pause"; 40 | } 41 | 42 | void DisableAutoPause::Enable() { 43 | AlterMemory(0x0044F478, std::array{ ret, nop, nop, nop, nop }, {}); 44 | } 45 | 46 | std::string_view RemoveDefaultPlants::Name() const noexcept { 47 | return "Remove-Default-Plants"; 48 | } 49 | 50 | void RemoveDefaultPlants::Enable() { 51 | const auto addr1{ *reinterpret_cast(BASE_ADDR) }; 52 | if (addr1 == 0) { 53 | return; 54 | } 55 | 56 | const auto addr2{ *reinterpret_cast(addr1 + 0x768) }; 57 | if (addr2 == 0) { 58 | return; 59 | } 60 | 61 | const auto plant_count{ *reinterpret_cast(addr2 + 0xB0) }; 62 | const auto begin_addr{ *reinterpret_cast(addr2 + 0xAC) }; 63 | const auto end_addr{ begin_addr + sizeof(PlantedPlant) * plant_count }; 64 | 65 | for (auto curr_addr{ begin_addr }; curr_addr != end_addr; 66 | curr_addr += sizeof(PlantedPlant)) { 67 | reinterpret_cast(curr_addr)->invalid = true; 68 | } 69 | } 70 | 71 | 72 | std::string_view AllowMultiProcess::Name() const noexcept { 73 | return "Allow-Multi-Process"; 74 | } 75 | 76 | void AllowMultiProcess::Enable() { 77 | AlterMemory(virtual_addr, std::array{ short_jmp }, {}); 78 | } 79 | 80 | void AllowMultiProcess::Enable(const std::string_view file_path) { 81 | if (file_path.empty()) { 82 | throw std::invalid_argument{ "The process path is empty." }; 83 | } 84 | 85 | std::fstream file{}; 86 | 87 | auto deleter{ [](std::fstream* const file) { 88 | assert(file != nullptr); 89 | file->close(); 90 | } }; 91 | 92 | std::unique_ptr raii{ &file, deleter }; 93 | 94 | file.open(file_path.data(), 95 | std::ios::in | std::ios::out | std::ios::binary); 96 | file.seekp(raw_offset); 97 | file.write(reinterpret_cast(&short_jmp), sizeof(short_jmp)); 98 | } 99 | 100 | } // namespace game::mod -------------------------------------------------------------------------------- /src/game/mod/mod.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file mod.h 3 | * @brief Modifications. 4 | * 5 | * @author Chen Zhenshuo (chenzs108@outlook.com) 6 | * @author Liu Guowen (liu.guowen@outlook.com) 7 | * @version 1.0 8 | * @date 2021-07-10 9 | * @par GitHub 10 | * https://github.com/czs108 11 | * @par 12 | * https://github.com/lgw1995 13 | */ 14 | 15 | #pragma once 16 | 17 | #include "interface.h" 18 | 19 | #include 20 | #include 21 | #include 22 | 23 | 24 | namespace game::mod { 25 | 26 | //! Set the amount of sun. 27 | class SetSunAmount : public Mod { 28 | public: 29 | SetSunAmount(std::size_t amount) noexcept; 30 | 31 | void Enable() override; 32 | 33 | std::string_view Name() const noexcept override; 34 | 35 | private: 36 | std::size_t amount_; 37 | }; 38 | 39 | //! Disable automatic pause during playing. 40 | class DisableAutoPause : public Mod { 41 | public: 42 | void Enable() override; 43 | 44 | std::string_view Name() const noexcept override; 45 | }; 46 | 47 | //! Remove all planted plants when an online level begins. 48 | class RemoveDefaultPlants : public Mod { 49 | public: 50 | void Enable() override; 51 | 52 | std::string_view Name() const noexcept override; 53 | }; 54 | 55 | //! Allow to run multiple game processes simultaneously. 56 | class AllowMultiProcess : public Mod { 57 | public: 58 | void Enable() override; 59 | 60 | /** 61 | * @brief Modify the game file to allow to run multiple processes simultaneously. 62 | * 63 | * @param file_path The path of @p PlantsVsZombies.exe. 64 | * 65 | * @exception std::invalid_argument The path is empty. 66 | */ 67 | void Enable(std::string_view file_path); 68 | 69 | std::string_view Name() const noexcept override; 70 | 71 | private: 72 | static constexpr std::intptr_t raw_offset{ 0x00153F1B }; 73 | static constexpr std::intptr_t virtual_addr{ 0x00553F1B }; 74 | }; 75 | 76 | } // namespace game::mod -------------------------------------------------------------------------------- /src/game/startup.cpp: -------------------------------------------------------------------------------- 1 | #include "startup.h" 2 | #include "mod/hook/hook.h" 3 | #include "mod/mod.h" 4 | #include "state.h" 5 | 6 | #include 7 | 8 | 9 | namespace game { 10 | 11 | Startup::Startup(const Role role, Config cfg) noexcept { 12 | state::cfg = std::move(cfg); 13 | state::role = role; 14 | } 15 | 16 | Startup::~Startup() noexcept { 17 | Stop(); 18 | } 19 | 20 | void Startup::Run() { 21 | mod::Loader{} 22 | .Add(std::make_unique()) 23 | .Add(std::make_unique()) 24 | .Add(std::make_unique()) 25 | .Load(); 26 | } 27 | 28 | 29 | void Startup::Stop() noexcept { 30 | state::conn.reset(); 31 | } 32 | 33 | } // namespace game -------------------------------------------------------------------------------- /src/game/state.cpp: -------------------------------------------------------------------------------- 1 | #include "state.h" 2 | 3 | 4 | namespace game::state { 5 | 6 | Role role{}; 7 | 8 | Config cfg{}; 9 | 10 | std::unique_ptr> conn{}; 11 | 12 | StoppableThread recv_thread{}; 13 | 14 | } // namespace game::state -------------------------------------------------------------------------------- /src/game/state.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file state.h 3 | * @brief Running status. 4 | * 5 | * @author Chen Zhenshuo (chenzs108@outlook.com) 6 | * @author Liu Guowen (liu.guowen@outlook.com) 7 | * @version 1.0 8 | * @date 2021-07-10 9 | * @par GitHub 10 | * https://github.com/czs108 11 | * @par 12 | * https://github.com/lgw1995 13 | */ 14 | 15 | #pragma once 16 | 17 | #include "config.h" 18 | 19 | #include "network/socket/tcp.h" 20 | 21 | #include 22 | #include 23 | 24 | 25 | namespace game::state { 26 | 27 | //! The player's role. 28 | extern Role role; 29 | 30 | //! The configuration. 31 | extern Config cfg; 32 | 33 | //! The network connection. 34 | extern std::unique_ptr> conn; 35 | 36 | //! The stoppable thread. 37 | struct StoppableThread { 38 | //! The thread handle. 39 | std::unique_ptr thread; 40 | 41 | //! A stop source that can send a stop request. 42 | std::unique_ptr stop_src; 43 | }; 44 | 45 | //! The network communication thread. 46 | extern StoppableThread recv_thread; 47 | 48 | } // namespace game::state -------------------------------------------------------------------------------- /src/network/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_library(network) 2 | 3 | set(HEADER_PATH ${PROJECT_SOURCE_DIR}/include/network) 4 | target_include_directories(network PRIVATE ${HEADER_PATH}) 5 | target_include_directories(network PUBLIC ${PROJECT_SOURCE_DIR}/include) 6 | 7 | target_sources(network 8 | PUBLIC 9 | ${HEADER_PATH}/ip_addr.h 10 | ${HEADER_PATH}/packet.h 11 | INTERFACE 12 | ${HEADER_PATH}/listener.h 13 | ${HEADER_PATH}/socket/tcp.h 14 | PRIVATE 15 | ${HEADER_PATH}/socket/basic.h 16 | socket/basic.cpp 17 | ip_addr.cpp 18 | packet.cpp 19 | ) 20 | 21 | target_link_libraries(network PUBLIC system) -------------------------------------------------------------------------------- /src/network/ip_addr.cpp: -------------------------------------------------------------------------------- 1 | #include "ip_addr.h" 2 | 3 | #include "system/windows_error.h" 4 | 5 | #pragma comment(lib, "ws2_32.lib") 6 | 7 | 8 | namespace net { 9 | 10 | namespace { 11 | 12 | //! A socket library initializer. 13 | struct Initializer { 14 | Initializer(); 15 | ~Initializer() noexcept; 16 | }; 17 | 18 | const Initializer initializer_{}; 19 | 20 | Initializer::Initializer() { 21 | WSADATA wsa{}; 22 | if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) { 23 | sys::ThrowWsaLastError(); 24 | } 25 | } 26 | 27 | Initializer::~Initializer() noexcept { 28 | WSACleanup(); 29 | } 30 | 31 | } // namespace 32 | 33 | Ipv4Addr::Ipv4Addr(const sockaddr_in& addr) noexcept : addr_{ addr } {} 34 | 35 | Ipv4Addr::Ipv4Addr(const std::string_view ip, const std::uint16_t port) { 36 | addr_.sin_family = version; 37 | addr_.sin_port = htons(port); 38 | if (inet_pton(version, ip.data(), &addr_.sin_addr) != 1) { 39 | sys::ThrowWsaLastError(); 40 | } 41 | } 42 | 43 | int Ipv4Addr::Version() const noexcept { 44 | return version; 45 | } 46 | 47 | std::size_t Ipv4Addr::Size() const noexcept { 48 | return sizeof(addr_); 49 | } 50 | 51 | const sockaddr* Ipv4Addr::Raw() const noexcept { 52 | return reinterpret_cast(&addr_); 53 | } 54 | 55 | 56 | Ipv6Addr::Ipv6Addr(const sockaddr_in6& addr) noexcept : addr_{ addr } {} 57 | 58 | Ipv6Addr::Ipv6Addr(const std::string_view ip, const std::uint16_t port) { 59 | addr_.sin6_family = version; 60 | addr_.sin6_port = htons(port); 61 | if (inet_pton(version, ip.data(), &addr_.sin6_addr) != 1) { 62 | sys::ThrowWsaLastError(); 63 | } 64 | } 65 | 66 | int Ipv6Addr::Version() const noexcept { 67 | return version; 68 | } 69 | 70 | std::size_t Ipv6Addr::Size() const noexcept { 71 | return sizeof(addr_); 72 | } 73 | 74 | const sockaddr* Ipv6Addr::Raw() const noexcept { 75 | return reinterpret_cast(&addr_); 76 | } 77 | 78 | } // namespace net -------------------------------------------------------------------------------- /src/network/packet.cpp: -------------------------------------------------------------------------------- 1 | #include "packet.h" 2 | 3 | 4 | namespace net { 5 | 6 | std::size_t Packet::Write(const std::span data) noexcept { 7 | buffer_.reserve(buffer_.size() + data.size()); 8 | for (const auto byte : data) { 9 | buffer_.push_back(byte); 10 | } 11 | 12 | return buffer_.size(); 13 | } 14 | 15 | std::span Packet::Read() noexcept { 16 | return buffer_; 17 | } 18 | 19 | } -------------------------------------------------------------------------------- /src/network/socket/basic.cpp: -------------------------------------------------------------------------------- 1 | #define NOMINMAX 2 | 3 | #include "socket/basic.h" 4 | 5 | #include 6 | 7 | 8 | namespace net { 9 | 10 | void CheckSizeLimit(const std::size_t size, const std::string_view msg) { 11 | if (size > static_cast(std::numeric_limits::max())) { 12 | throw std::overflow_error{ msg.data() }; 13 | } 14 | } 15 | 16 | } -------------------------------------------------------------------------------- /src/system/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_library(system) 2 | 3 | set(HEADER_PATH ${PROJECT_SOURCE_DIR}/include/system) 4 | target_include_directories(system PRIVATE ${HEADER_PATH}) 5 | target_include_directories(system PUBLIC ${PROJECT_SOURCE_DIR}/include) 6 | 7 | target_sources(system 8 | PUBLIC 9 | ${HEADER_PATH}/windows_error.h 10 | ${HEADER_PATH}/memory.h 11 | PRIVATE 12 | windows_error.cpp 13 | memory.cpp 14 | ) -------------------------------------------------------------------------------- /src/system/memory.cpp: -------------------------------------------------------------------------------- 1 | #include "memory.h" 2 | #include "windows_error.h" 3 | 4 | #include 5 | #include 6 | 7 | 8 | namespace sys { 9 | 10 | void CheckNullPointer(const std::intptr_t addr) { 11 | if (addr == 0) { 12 | throw std::invalid_argument{ "The pointer is null." }; 13 | } 14 | } 15 | 16 | void SetMemoryWritable(const std::intptr_t addr, const std::size_t size) { 17 | DWORD old_protect{ 0 }; 18 | if (VirtualProtect(reinterpret_cast(addr), size, 19 | PAGE_EXECUTE_READWRITE, &old_protect) 20 | == FALSE) { 21 | ThrowLastError(); 22 | } 23 | } 24 | 25 | 26 | void AlterMemory(const std::intptr_t addr, 27 | const std::span new_bytes, 28 | const std::span origin_bytes) { 29 | SetMemoryWritable(addr, new_bytes.size_bytes()); 30 | 31 | if (!origin_bytes.empty()) { 32 | if (origin_bytes.size_bytes() < new_bytes.size_bytes()) { 33 | throw std::length_error{ 34 | "The buffer for original bytes is too small." 35 | }; 36 | } 37 | 38 | std::memcpy(origin_bytes.data(), reinterpret_cast(addr), 39 | new_bytes.size_bytes()); 40 | } 41 | 42 | std::memcpy(reinterpret_cast(addr), new_bytes.data(), 43 | new_bytes.size_bytes()); 44 | } 45 | 46 | 47 | std::array FormatJmpBytes(const std::intptr_t from, 48 | const std::intptr_t to) noexcept { 49 | std::array bytes{ jmp }; 50 | const auto offset{ to - from - jmp_len }; 51 | std::memcpy(bytes.data() + 1, &offset, sizeof(offset)); 52 | return bytes; 53 | } 54 | 55 | 56 | std::intptr_t GetFuncEntryAddr(const std::intptr_t addr) noexcept { 57 | const auto op{ *reinterpret_cast(addr) }; 58 | 59 | if (op != jmp) { 60 | return addr; 61 | } else { 62 | const auto offset{ *reinterpret_cast( 63 | addr + sizeof(std::byte)) }; 64 | return addr + offset + jmp_len; 65 | } 66 | } 67 | 68 | } // namespace sys -------------------------------------------------------------------------------- /src/system/windows_error.cpp: -------------------------------------------------------------------------------- 1 | #include "windows_error.h" 2 | 3 | #include 4 | 5 | #include 6 | 7 | #pragma comment(lib, "ws2_32.lib") 8 | 9 | 10 | namespace sys { 11 | 12 | [[noreturn]] void ThrowWsaLastError() { 13 | throw std::system_error{ WSAGetLastError(), std::system_category() }; 14 | } 15 | 16 | [[noreturn]] void ThrowLastError() { 17 | throw std::system_error{ static_cast(GetLastError()), std::system_category() }; 18 | } 19 | 20 | } // namespace sys --------------------------------------------------------------------------------