├── .flake8 ├── .github └── workflows │ └── python-package.yml ├── .gitignore ├── .readthedocs.yml ├── LICENSE ├── README.md ├── docs ├── Makefile ├── conf.py ├── index.rst ├── make.bat ├── requirements.txt └── source │ ├── modules.rst │ └── tminterface.rst ├── examples ├── bf_optimize_pos.py ├── bf_optimize_speed.py ├── custom_command.py ├── print_values.py ├── process_async.py ├── repeat_simulation.py └── restore_state.py ├── requirements.txt ├── setup.cfg ├── setup.py └── tminterface ├── __init__.py ├── client.py ├── commandlist.py ├── constants.py ├── eventbuffer.py ├── interface.py ├── structs.py └── util.py /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | extend-ignore = E501, E221, F405, F403 -------------------------------------------------------------------------------- /.github/workflows/python-package.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python 3 | 4 | name: Python package 5 | 6 | on: 7 | push: 8 | branches: [ "master" ] 9 | pull_request: 10 | branches: [ "master" ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | python-version: ["3.8", "3.9", "3.10"] 20 | 21 | steps: 22 | - uses: actions/checkout@v3 23 | - name: Set up Python ${{ matrix.python-version }} 24 | uses: actions/setup-python@v3 25 | with: 26 | python-version: ${{ matrix.python-version }} 27 | - name: Install dependencies 28 | run: | 29 | python -m pip install --upgrade pip 30 | python -m pip install flake8 31 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 32 | - name: Lint with flake8 33 | run: | 34 | # stop the build if there are Python syntax errors or undefined names 35 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 36 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 37 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | *.py[cod] 3 | docs/_static 4 | docs/_templates 5 | docs/_build 6 | *.egg-info -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | # Read the Docs configuration file for Sphinx projects 2 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 3 | 4 | # Required 5 | version: 2 6 | 7 | build: 8 | os: ubuntu-22.04 9 | tools: 10 | python: "3.12" 11 | 12 | # Build documentation in the "docs/" directory with Sphinx 13 | sphinx: 14 | configuration: docs/conf.py 15 | # You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs 16 | # builder: "dirhtml" 17 | # Fail on all warnings to avoid broken references 18 | # fail_on_warning: true 19 | 20 | 21 | python: 22 | install: 23 | - requirements: docs/requirements.txt -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Python client for TMInterface 2 | [![Documentation Status](https://readthedocs.org/projects/tminterface/badge/?version=latest)](https://tminterface.readthedocs.io/en/latest/?badge=latest) 3 | 4 | This is the Python client for TMInterface, which allows for modifying TrackMania state through the TMInterface server API. 5 | 6 | **NOTE:** This API is **only** working on TMInterface versions < 2.0.0. From version 2.0.0, TMInterface introduced a [new plugin API with AngelScript](https://donadigo.com/tminterface/plugins/intro). If you still wish to use this API, download the [1.4.3 version of TMInterface](https://donadigo.com/files/TMInterface/TMInterface_1.4.3_Setup.exe). 7 | 8 | *Consider this API legacy and not maintained.* 9 | 10 | Install the package with pip: `pip install tminterface`. 11 | 12 | Documentation is available on [Read the Docs](https://tminterface.readthedocs.io/en/latest/). -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | import os 14 | import sys 15 | sys.path.insert(0, os.path.abspath('.')) 16 | sys.path.insert(0, os.path.abspath('../')) 17 | 18 | 19 | # -- Project information ----------------------------------------------------- 20 | 21 | project = 'TMInterface Client for Python' 22 | copyright = '2023, Adam Bieńkowski' 23 | author = 'Adam Bieńkowski' 24 | 25 | # The full version, including alpha/beta/rc tags 26 | release = '1.0.0' 27 | 28 | 29 | # -- General configuration --------------------------------------------------- 30 | 31 | # Add any Sphinx extension module names here, as strings. They can be 32 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 33 | # ones. 34 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.napoleon'] 35 | 36 | # Add any paths that contain templates here, relative to this directory. 37 | templates_path = [] 38 | 39 | # List of patterns, relative to source directory, that match files and 40 | # directories to ignore when looking for source files. 41 | # This pattern also affects html_static_path and html_extra_path. 42 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 43 | 44 | 45 | # -- Options for HTML output ------------------------------------------------- 46 | 47 | # The theme to use for HTML and HTML Help pages. See the documentation for 48 | # a list of builtin themes. 49 | # 50 | html_theme = 'sphinx_rtd_theme' 51 | 52 | # Add any paths that contain custom static files (such as style sheets) here, 53 | # relative to this directory. They are copied after the builtin static files, 54 | # so a file named "default.css" will overwrite the builtin "default.css". 55 | html_static_path = [] 56 | 57 | autodoc_mock_imports = ['bytefield', 'numpy', 'ctypes'] 58 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. TMInterface Client for Python documentation master file, created by 2 | sphinx-quickstart on Tue Mar 8 16:17:05 2022. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to TMInterface Client for Python's documentation! 7 | ========================================================= 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | :caption: Contents: 12 | 13 | 14 | 15 | Indices and tables 16 | ================== 17 | 18 | * :ref:`genindex` 19 | * :ref:`modindex` 20 | * :ref:`search` 21 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.https://www.sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx-rtd-theme==2.0.0 -------------------------------------------------------------------------------- /docs/source/modules.rst: -------------------------------------------------------------------------------- 1 | tminterface 2 | =========== 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | tminterface 8 | -------------------------------------------------------------------------------- /docs/source/tminterface.rst: -------------------------------------------------------------------------------- 1 | tminterface package 2 | =================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | tminterface.client module 8 | ------------------------- 9 | 10 | .. automodule:: tminterface.client 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | tminterface.commandlist module 16 | ------------------------------ 17 | 18 | .. automodule:: tminterface.commandlist 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | tminterface.constants module 24 | ---------------------------- 25 | 26 | .. automodule:: tminterface.constants 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | tminterface.eventbuffer module 32 | ------------------------------ 33 | 34 | .. automodule:: tminterface.eventbuffer 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | 39 | tminterface.interface module 40 | ---------------------------- 41 | 42 | .. automodule:: tminterface.interface 43 | :members: 44 | :undoc-members: 45 | :show-inheritance: 46 | 47 | tminterface.structs module 48 | -------------------------- 49 | 50 | .. automodule:: tminterface.structs 51 | :members: 52 | :undoc-members: 53 | :show-inheritance: 54 | 55 | tminterface.util module 56 | ----------------------- 57 | 58 | .. automodule:: tminterface.util 59 | :members: 60 | :undoc-members: 61 | :show-inheritance: 62 | 63 | Module contents 64 | --------------- 65 | 66 | .. automodule:: tminterface 67 | :members: 68 | :undoc-members: 69 | :show-inheritance: 70 | -------------------------------------------------------------------------------- /examples/bf_optimize_pos.py: -------------------------------------------------------------------------------- 1 | from tminterface.structs import BFEvaluationDecision, BFEvaluationInfo, BFEvaluationResponse, BFPhase 2 | from tminterface.interface import TMInterface 3 | from tminterface.client import Client, run_client 4 | import sys 5 | 6 | 7 | # Example optimizing X position on A01-Race 8 | class MainClient(Client): 9 | def __init__(self) -> None: 10 | self.current_time = 0 11 | self.do_accept = False 12 | self.force_accept = False 13 | self.lowest_time = 0 14 | self.phase = BFPhase.INITIAL 15 | self.current_ending_pos = [0, 0, 0] 16 | self.target_ending_pos = [0, 0, 0] 17 | super(MainClient, self).__init__() 18 | 19 | def on_registered(self, iface: TMInterface) -> None: 20 | print(f'Registered to {iface.server_name}') 21 | iface.execute_command('set controller bruteforce') 22 | iface.execute_command('set bf_search_forever true') 23 | 24 | def on_simulation_begin(self, iface: TMInterface): 25 | self.lowest_time = iface.get_event_buffer().events_duration 26 | 27 | def on_bruteforce_evaluate(self, iface: TMInterface, info: BFEvaluationInfo) -> BFEvaluationResponse: 28 | self.current_time = info.time 29 | self.phase = info.phase 30 | 31 | response = BFEvaluationResponse() 32 | response.decision = BFEvaluationDecision.DO_NOTHING 33 | 34 | if (self.do_accept and self.current_ending_pos[0] < self.target_ending_pos[0]) or self.force_accept: 35 | print(self.current_ending_pos[0], self.target_ending_pos[0]) 36 | response.decision = BFEvaluationDecision.ACCEPT 37 | elif self.current_time > self.lowest_time: 38 | response.decision = BFEvaluationDecision.REJECT 39 | self.current_ending_pos = self.target_ending_pos 40 | 41 | self.do_accept = False 42 | self.force_accept = False 43 | 44 | return response 45 | 46 | def on_checkpoint_count_changed(self, iface: TMInterface, current: int, target: int): 47 | if current == target: 48 | if self.phase == BFPhase.INITIAL: 49 | self.lowest_time = self.current_time 50 | self.target_ending_pos = iface.get_simulation_state().position 51 | elif self.phase == BFPhase.SEARCH: 52 | self.current_ending_pos = iface.get_simulation_state().position 53 | if self.current_time <= self.lowest_time: 54 | self.do_accept = True 55 | 56 | if self.current_time < self.lowest_time: 57 | self.force_accept = True 58 | 59 | 60 | def main(): 61 | server_name = f'TMInterface{sys.argv[1]}' if len(sys.argv) > 1 else 'TMInterface0' 62 | print(f'Connecting to {server_name}...') 63 | run_client(MainClient(), server_name) 64 | 65 | 66 | if __name__ == '__main__': 67 | main() 68 | -------------------------------------------------------------------------------- /examples/bf_optimize_speed.py: -------------------------------------------------------------------------------- 1 | from tminterface.structs import BFEvaluationDecision, BFEvaluationInfo, BFEvaluationResponse, BFPhase 2 | from tminterface.interface import TMInterface 3 | from tminterface.client import Client, run_client 4 | import sys 5 | import numpy as np 6 | 7 | 8 | # Example optimizing collective speed from all ticks. Runs with overall more 9 | # speed will be accepted, rest - rejected. 10 | class MainClient(Client): 11 | def __init__(self) -> None: 12 | self.current_time = 0 13 | self.do_accept = False 14 | self.lowest_time = 0 15 | self.current_speeds = [] 16 | self.target_speeds = [] 17 | self.phase = BFPhase.INITIAL 18 | super(MainClient, self).__init__() 19 | 20 | def on_registered(self, iface: TMInterface) -> None: 21 | print(f'Registered to {iface.server_name}') 22 | iface.execute_command('set controller bruteforce') 23 | iface.execute_command('set bf_search_forever true') 24 | 25 | def on_simulation_begin(self, iface: TMInterface): 26 | self.lowest_time = iface.get_event_buffer().events_duration 27 | 28 | def on_bruteforce_evaluate(self, iface: TMInterface, info: BFEvaluationInfo) -> BFEvaluationResponse: 29 | self.current_time = info.time 30 | self.phase = info.phase 31 | 32 | response = BFEvaluationResponse() 33 | response.decision = BFEvaluationDecision.DO_NOTHING 34 | 35 | if self.current_time >= 10: 36 | state = iface.get_simulation_state() 37 | if self.phase == BFPhase.INITIAL: 38 | if self.current_time == 10: 39 | self.target_speeds = [] 40 | 41 | self.target_speeds.append(np.linalg.norm(state.velocity)) 42 | 43 | else: 44 | index = int((self.current_time - 10) / 10) 45 | if index < len(self.current_speeds): 46 | self.current_speeds[index] = np.linalg.norm(state.velocity) 47 | 48 | if self.do_accept and sum(self.current_speeds) > sum(self.target_speeds): 49 | print(sum(self.current_speeds), sum(self.target_speeds)) 50 | response.decision = BFEvaluationDecision.ACCEPT 51 | elif self.current_time > self.lowest_time: 52 | response.decision = BFEvaluationDecision.REJECT 53 | self.current_speeds = self.target_speeds[:] 54 | 55 | self.do_accept = False 56 | 57 | return response 58 | 59 | def on_checkpoint_count_changed(self, iface: TMInterface, current: int, target: int): 60 | if current == target: 61 | if self.phase == BFPhase.INITIAL: 62 | self.current_speeds = self.target_speeds[:] 63 | self.lowest_time = self.current_time 64 | elif self.phase == BFPhase.SEARCH: 65 | if self.current_time <= self.lowest_time: 66 | self.do_accept = True 67 | 68 | 69 | def main(): 70 | server_name = f'TMInterface{sys.argv[1]}' if len(sys.argv) > 1 else 'TMInterface0' 71 | print(f'Connecting to {server_name}...') 72 | run_client(MainClient(), server_name) 73 | 74 | 75 | if __name__ == '__main__': 76 | main() 77 | -------------------------------------------------------------------------------- /examples/custom_command.py: -------------------------------------------------------------------------------- 1 | from tminterface.interface import TMInterface 2 | from tminterface.client import Client, run_client 3 | import sys 4 | 5 | 6 | class MainClient(Client): 7 | def __init__(self) -> None: 8 | super(MainClient, self).__init__() 9 | 10 | def on_registered(self, iface: TMInterface) -> None: 11 | print(f'Registered to {iface.server_name}') 12 | iface.register_custom_command('echo') 13 | 14 | def on_custom_command(self, iface, time_from: int, time_to: int, command: str, args: list): 15 | # Usage: echo [message] [severity] 16 | # echo "Something like this" 17 | # echo "An error message" error 18 | if command == 'echo': 19 | if len(args) > 0: 20 | severity = 'log' if len(args) == 1 else args[1] 21 | iface.log(args[0], severity) 22 | else: 23 | iface.log('echo takes at least one argument', 'error') 24 | 25 | 26 | def main(): 27 | server_name = f'TMInterface{sys.argv[1]}' if len(sys.argv) > 1 else 'TMInterface0' 28 | print(f'Connecting to {server_name}...') 29 | run_client(MainClient(), server_name) 30 | 31 | 32 | if __name__ == '__main__': 33 | main() 34 | -------------------------------------------------------------------------------- /examples/print_values.py: -------------------------------------------------------------------------------- 1 | from tminterface.interface import TMInterface 2 | from tminterface.client import Client, run_client 3 | import sys 4 | 5 | 6 | class MainClient(Client): 7 | def __init__(self) -> None: 8 | super(MainClient, self).__init__() 9 | 10 | def on_registered(self, iface: TMInterface) -> None: 11 | print(f'Registered to {iface.server_name}') 12 | 13 | def on_run_step(self, iface: TMInterface, _time: int): 14 | if _time >= 0: 15 | state = iface.get_simulation_state() 16 | 17 | print( 18 | f'Time: {_time}\n' 19 | f'Display Speed: {state.display_speed}\n' 20 | f'Position: {state.position}\n' 21 | f'Velocity: {state.velocity}\n' 22 | f'YPW: {state.yaw_pitch_roll}\n' 23 | ) 24 | 25 | 26 | def main(): 27 | server_name = f'TMInterface{sys.argv[1]}' if len(sys.argv) > 1 else 'TMInterface0' 28 | print(f'Connecting to {server_name}...') 29 | run_client(MainClient(), server_name) 30 | 31 | 32 | if __name__ == '__main__': 33 | main() 34 | -------------------------------------------------------------------------------- /examples/process_async.py: -------------------------------------------------------------------------------- 1 | from tminterface.interface import TMInterface 2 | from tminterface.client import Client 3 | import sys 4 | 5 | import time 6 | import signal 7 | 8 | 9 | class MainClient(Client): 10 | def __init__(self) -> None: 11 | super(MainClient, self).__init__() 12 | self.time = 0 13 | self.finished = False 14 | 15 | def on_registered(self, iface: TMInterface) -> None: 16 | print(f'Registered to {iface.server_name}') 17 | 18 | def on_deregistered(self, iface: TMInterface): 19 | print(f'Deregistered from {iface.server_name}') 20 | 21 | def on_run_step(self, iface: TMInterface, _time: int): 22 | self.time = _time 23 | 24 | def on_checkpoint_count_changed(self, iface: TMInterface, current: int, target: int): 25 | if current == target: 26 | self.finished = True 27 | 28 | 29 | def main(): 30 | server_name = f'TMInterface{sys.argv[1]}' if len(sys.argv) > 1 else 'TMInterface0' 31 | print(f'Connecting to {server_name}...') 32 | client = MainClient() 33 | iface = TMInterface(server_name) 34 | 35 | def handler(signum, frame): 36 | iface.close() 37 | quit() 38 | 39 | signal.signal(signal.SIGBREAK, handler) 40 | signal.signal(signal.SIGINT, handler) 41 | iface.register(client) 42 | 43 | while not iface.registered: 44 | time.sleep(0) 45 | 46 | last_finished = False 47 | last_time = 0 48 | while iface.registered: 49 | if last_finished != client.finished: 50 | last_finished = client.finished 51 | if last_finished: 52 | print('Finished') 53 | 54 | if client.time != last_time: 55 | last_time = client.time 56 | 57 | if client.time % 1000 == 0: 58 | print(client.time) 59 | time.sleep(0) 60 | 61 | 62 | if __name__ == '__main__': 63 | main() 64 | -------------------------------------------------------------------------------- /examples/repeat_simulation.py: -------------------------------------------------------------------------------- 1 | from tminterface.interface import TMInterface 2 | from tminterface.client import Client, run_client 3 | import sys 4 | 5 | 6 | class MainClient(Client): 7 | def __init__(self) -> None: 8 | self.state = None 9 | self.finished = False 10 | self.race_time = 0 11 | super(MainClient, self).__init__() 12 | 13 | def on_registered(self, iface: TMInterface) -> None: 14 | print(f'Registered to {iface.server_name}') 15 | 16 | def on_simulation_begin(self, iface: TMInterface): 17 | iface.remove_state_validation() 18 | self.finished = False 19 | 20 | def on_simulation_step(self, iface: TMInterface, _time: int): 21 | self.race_time = _time 22 | if self.race_time == 0: 23 | self.state = iface.get_simulation_state() 24 | 25 | if self.finished: 26 | iface.rewind_to_state(self.state) 27 | self.finished = False 28 | 29 | def on_checkpoint_count_changed(self, iface: TMInterface, current: int, target: int): 30 | print(f'Reached checkpoint {current}/{target}') 31 | if current == target: 32 | print(f'Finished the race at {self.race_time}') 33 | self.finished = True 34 | iface.prevent_simulation_finish() 35 | 36 | def on_simulation_end(self, iface, result: int): 37 | print('Simulation finished') 38 | 39 | 40 | def main(): 41 | server_name = f'TMInterface{sys.argv[1]}' if len(sys.argv) > 1 else 'TMInterface0' 42 | print(f'Connecting to {server_name}...') 43 | run_client(MainClient(), server_name) 44 | 45 | 46 | if __name__ == '__main__': 47 | main() 48 | -------------------------------------------------------------------------------- /examples/restore_state.py: -------------------------------------------------------------------------------- 1 | from tminterface.interface import TMInterface 2 | from tminterface.client import Client, run_client 3 | import sys 4 | 5 | 6 | class MainClient(Client): 7 | def __init__(self) -> None: 8 | self.state = None 9 | super(MainClient, self).__init__() 10 | 11 | def on_registered(self, iface: TMInterface) -> None: 12 | print(f'Registered to {iface.server_name}') 13 | 14 | def on_run_step(self, iface: TMInterface, _time: int): 15 | if _time == 1000: 16 | iface.set_input_state(right=True, accelerate=True, brake=True) 17 | 18 | if _time == 500: 19 | self.state = iface.get_simulation_state() 20 | 21 | if _time == 5000 and self.state: 22 | iface.rewind_to_state(self.state) 23 | 24 | 25 | def main(): 26 | server_name = f'TMInterface{sys.argv[1]}' if len(sys.argv) > 1 else 'TMInterface0' 27 | print(f'Connecting to {server_name}...') 28 | run_client(MainClient(), server_name) 29 | 30 | 31 | if __name__ == '__main__': 32 | main() 33 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | bytefield==1.0.2 -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from distutils.core import setup 2 | setup( 3 | name = 'tminterface', 4 | packages = ['tminterface'], 5 | version = '1.0.2', 6 | license='GPL3', 7 | description = 'A client for TMInterface, a TrackMania TAS tool', 8 | author = 'Adam Bieńkowski', 9 | author_email = 'donadigos159@gmail.com', 10 | url = 'https://github.com/donadigo/TMInterfaceClientPython', 11 | download_url = 'https://github.com/donadigo/TMInterfaceClientPython/archive/refs/tags/0.6.tar.gz', 12 | keywords = ['TMInterface', 'client', 'TrackMania'], 13 | install_requires=[ 14 | 'bytefield==1.0.2', 15 | ], 16 | classifiers=[ 17 | 'Development Status :: 4 - Beta', 18 | 'Intended Audience :: Developers', 19 | 'Topic :: Software Development :: Build Tools', 20 | 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', 21 | 'Programming Language :: Python :: 3', 22 | 'Programming Language :: Python :: 3.4', 23 | 'Programming Language :: Python :: 3.5', 24 | 'Programming Language :: Python :: 3.6', 25 | ], 26 | ) -------------------------------------------------------------------------------- /tminterface/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/donadigo/TMInterfaceClientPython/ce73dd80c33a9e7b48e9601256e94d8a6bf15a92/tminterface/__init__.py -------------------------------------------------------------------------------- /tminterface/client.py: -------------------------------------------------------------------------------- 1 | 2 | from tminterface.structs import BFEvaluationInfo, BFEvaluationResponse 3 | from tminterface.constants import DEFAULT_SERVER_SIZE 4 | import signal 5 | import time 6 | import sys 7 | 8 | 9 | class Client(object): 10 | def __init__(self): 11 | pass 12 | 13 | def on_registered(self, iface): 14 | """ 15 | A callback that the client has registered to a TMInterface instance. 16 | 17 | Args: 18 | iface (TMInterface): the TMInterface object that has been registered 19 | """ 20 | pass 21 | 22 | def on_deregistered(self, iface): 23 | """ 24 | A callback that the client has been deregistered from a TMInterface instance. 25 | This can be emitted when the game closes, the client does not respond in the timeout window, 26 | or the user manually deregisters the client with the `deregister` command. 27 | 28 | Args: 29 | iface (TMInterface): the TMInterface object that has been deregistered 30 | """ 31 | pass 32 | 33 | def on_shutdown(self, iface): 34 | """ 35 | A callback that the TMInterface server is shutting down. This is emitted when the game is closed. 36 | 37 | Args: 38 | iface (TMInterface): the TMInterface object that has been closed 39 | """ 40 | pass 41 | 42 | def on_run_step(self, iface, _time: int): 43 | """ 44 | Called on each "run" step (physics tick). This method will be called only in normal races and not 45 | when validating a replay. 46 | 47 | Args: 48 | iface (TMInterface): the TMInterface object 49 | """ 50 | pass 51 | 52 | def on_simulation_begin(self, iface): 53 | """ 54 | Called when a new simulation session is started (when validating a replay). 55 | 56 | Args: 57 | iface (TMInterface): the TMInterface object 58 | """ 59 | pass 60 | 61 | def on_simulation_step(self, iface, _time: int): 62 | """ 63 | Called on each simulation step (physics tick). This method will be called only when validating a replay. 64 | 65 | Args: 66 | iface (TMInterface): the TMInterface object 67 | """ 68 | pass 69 | 70 | def on_simulation_end(self, iface, result: int): 71 | """ 72 | Called when a new simulation session is ended (when validating a replay). 73 | 74 | Args: 75 | iface (TMInterface): the TMInterface object 76 | """ 77 | pass 78 | 79 | def on_checkpoint_count_changed(self, iface, current: int, target: int): 80 | """ 81 | Called when the current checkpoint count changed (a new checkpoint has been passed by the vehicle). 82 | The `current` and `target` parameters account for the total amount of checkpoints to be collected, 83 | taking lap count into consideration. 84 | 85 | Args: 86 | iface (TMInterface): the TMInterface object 87 | current (int): the current amount of checkpoints passed 88 | target (int): the total amount of checkpoints on the map (including finish) 89 | """ 90 | pass 91 | 92 | def on_laps_count_changed(self, iface, current: int): 93 | """ 94 | Called when the current lap count changed (a new lap has been passed). 95 | 96 | Args: 97 | iface (TMInterface): the TMInterface object 98 | current (int): the current amount of laps passed 99 | """ 100 | pass 101 | 102 | def on_custom_command(self, iface, time_from: int, time_to: int, command: str, args: list): 103 | """ 104 | Called when a custom command has been executed by the user. 105 | 106 | Args: 107 | iface (TMInterface): the TMInterface object 108 | time_from (int): if provided by the user, the starting time of the command, otherwise -1 109 | time_to (int): if provided by the user, the ending time of the command, otherwise -1 110 | command (str): the command name being executed 111 | args (list): the argument list provided by the user 112 | """ 113 | pass 114 | 115 | def on_bruteforce_evaluate(self, iface, info: BFEvaluationInfo) -> BFEvaluationResponse: 116 | """ 117 | Called on each bruteforce physics step iteration. This method will only be called when 118 | the bruteforce script is enabled in TMInterface. Used for implementing custom evaluation 119 | strategies. For greater control over the simulation, use the Client.on_simulation_step method instead. 120 | 121 | Args: 122 | iface (TMInterface): the TMInterface object 123 | info (BFEvaluationInfo): the info about the current bruteforce settings and race time 124 | 125 | Returns: 126 | None if the bruteforce script should continue its builtin evaluation or a BFEvaluationResponse 127 | that signifies what the script should do. 128 | """ 129 | return None 130 | 131 | def on_client_exception(self, iface, exception: Exception): 132 | """ 133 | Called when a client exception is thrown. This can happen if opening the shared file fails, or reading from 134 | it fails. 135 | 136 | Args: 137 | iface (TMInterface): the TMInterface object 138 | exception (Exception): the exception being thrown 139 | """ 140 | print(f'[Client] Exception reported: {exception}') 141 | 142 | 143 | def run_client(client: Client, server_name: str = 'TMInterface0', buffer_size=DEFAULT_SERVER_SIZE): 144 | """ 145 | Connects to a server with the specified server name and registers the client instance. 146 | The function closes the connection on SIGBREAK and SIGINT signals and will block 147 | until the client is deregistered in any way. You can set the buffer size yourself to use for 148 | the connection, by specifying the buffer_size parameter. Using a custom size requires 149 | launching TMInterface with the /serversize command line parameter: TMInterface.exe /serversize=size. 150 | 151 | Args: 152 | client (Client): the client instance to register 153 | server_name (str): the server name to connect to, TMInterface0 by default 154 | buffer_size (int): the buffer size to use, the default size is defined by tminterface.constants.DEFAULT_SERVER_SIZE 155 | """ 156 | from .interface import TMInterface 157 | 158 | iface = TMInterface(server_name, buffer_size) 159 | 160 | def handler(signum, frame): 161 | iface.close() 162 | 163 | if sys.platform == 'win32': 164 | signal.signal(signal.SIGBREAK, handler) 165 | signal.signal(signal.SIGINT, handler) 166 | 167 | iface.register(client) 168 | 169 | while iface.running: 170 | time.sleep(0) 171 | -------------------------------------------------------------------------------- /tminterface/commandlist.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from enum import IntEnum 3 | from io import IOBase 4 | 5 | BOT_COMMANDS = ['press', 'rel', 'steer', 'gas'] 6 | BOT_INPUT_TYPES = ['up', 'down', 'left', 'right', 'enter', 'delete', 'horn', 'steer', 'gas'] 7 | 8 | 9 | class InputType(IntEnum): 10 | """ 11 | The InputType enum represents an input type that is coupled with its state. 12 | """ 13 | UP = 0 14 | DOWN = 1 15 | LEFT = 2 16 | RIGHT = 3 17 | RESPAWN = 4 18 | RESET = 5 19 | HORN = 6 20 | STEER = 7 21 | GAS = 8 22 | UNKNOWN = 9 23 | 24 | @staticmethod 25 | def from_str(s: str): 26 | """ 27 | Converts a script action string into an InputType. 28 | 29 | If the action is invalid, InputType.UNKNOWN will be returned. 30 | 31 | Args: 32 | s (str): the string to convert 33 | 34 | Returns: 35 | InputType: the converted input type 36 | """ 37 | s = s.lower() 38 | if s not in BOT_INPUT_TYPES: 39 | return InputType.UNKNOWN 40 | 41 | return InputType(BOT_INPUT_TYPES.index(s)) 42 | 43 | def to_str(self) -> str: 44 | if int(self) < len(BOT_INPUT_TYPES): 45 | return BOT_INPUT_TYPES[int(self)] 46 | else: 47 | return 'unknown' 48 | 49 | 50 | class BaseCommand: 51 | """ 52 | The BaseCommand class is a base class for all command classes such as Command, TimedCommand and InputCommand. 53 | """ 54 | def to_script(self) -> str: 55 | """ 56 | Converts a command to a script string line. 57 | 58 | Returns: 59 | str: the script snippet that represents this command 60 | """ 61 | pass 62 | 63 | 64 | @dataclass 65 | class Command(BaseCommand): 66 | """ 67 | A Command represents an immediate command that is executed immediately by TMInterface whenever it's encountered. 68 | """ 69 | args: list 70 | 71 | def to_script(self) -> str: 72 | return ' '.join(self.args) 73 | 74 | 75 | @dataclass 76 | class InputCommand(BaseCommand): 77 | """ 78 | The InputCommand class specifically represents a command that is used to inject any kind of input into the game. 79 | 80 | An input does not contain any arguments. Instead, the class defines an input with its type and state. 81 | InputCommand's can be converted from an instance of TimedCommand. 82 | 83 | InputCommand's do not need to be stored to describe a TMInterface script, they are however automatically added 84 | by the CommandList for an easy access & manipulation of the input sequence. 85 | """ 86 | timestamp: int 87 | input_type: InputType 88 | state: int 89 | 90 | def to_script(self) -> str: 91 | if self.input_type == InputType.STEER or self.input_type == InputType.GAS: 92 | return f'{self.timestamp} {self.input_type.to_str()} {self.state}' 93 | elif self.input_type == InputType.UNKNOWN: 94 | return f'# {self.timestamp} [unknown] {int(self.state)}' 95 | else: 96 | action = 'press' if self.state else 'rel' 97 | return f'{self.timestamp} {action} {self.input_type.to_str()}' 98 | 99 | 100 | @dataclass 101 | class TimedCommand(Command): 102 | """ 103 | A TimedCommand describes any command that is executed at a specific timestamp. 104 | 105 | The TimedCommand can represent any command, including any input commands. 106 | A command with a ranged timestamp will be always converted to two TimedCommand instances, 107 | where the earliest command will have is_ending set to False and the latest, to True. 108 | """ 109 | timestamp: int 110 | is_ending: bool 111 | 112 | def to_input_command(self) -> InputCommand: 113 | """ 114 | Converts a TimedCommand to an InputCommand if possible. 115 | 116 | If the conversion fails or the TimedCommand is not a valid input command, None is returned. 117 | 118 | Returns: 119 | InputCommand: the converted InputCommand, or None if the conversion failed 120 | """ 121 | if len(self.args) < 2 or self.args[0].lower() not in BOT_COMMANDS: 122 | return None 123 | 124 | state = 0 125 | action = self.args[0].lower() 126 | input_type = InputType.UNKNOWN 127 | if action == 'press' or action == 'rel': 128 | if action == 'press' and not self.is_ending: 129 | state = 1 130 | 131 | input_type = InputType.from_str(self.args[1]) 132 | elif action == 'steer' or action == 'gas': 133 | input_type = InputType.from_str(action) 134 | 135 | if not self.is_ending: 136 | try: 137 | state = int(self.args[1]) 138 | except ValueError: 139 | return None 140 | else: 141 | return None 142 | 143 | return InputCommand(self.timestamp, input_type, state) 144 | 145 | def to_script(self) -> str: 146 | input_command = self.to_input_command() 147 | if input_command: 148 | return input_command.to_script() 149 | 150 | return f'{self.timestamp} {super().to_script()}' 151 | 152 | 153 | class CommandList(object): 154 | """ 155 | A CommandList represents a list of TMInterface commands usually forming a script which can contain immediate 156 | and timed commands. 157 | 158 | A CommandList can be loaded by providing a file handle to an existing script file or from a string. 159 | You can also construct an empty CommandList to add your own commands to then convert them 160 | into a valid TMInterface script. 161 | 162 | If a resource is provided, the class will attempt to parse all of its contents into immediate and timed commands. 163 | You can use CommandList.to_script() to convert all the commands back into a valid TMInterface script. 164 | If any command cannot be converted, it will be commented out. 165 | 166 | The class fully supports parsing commands with quoted arguments and inline comments and can be used 167 | to generate new script files. 168 | 169 | Args: 170 | obj: the resource that needs to be parsed, either: 171 | 172 | a file handle opened with open() 173 | 174 | a string containing the command list 175 | 176 | None to create an empty list 177 | 178 | Attributes: 179 | commands (list): the list containing all immediate commands 180 | timed_commands (list): the list containing all timed commands, including input commands 181 | content (str): the script string that was used to construct the CommandList 182 | """ 183 | def __init__(self, obj=None): 184 | self.commands = [] 185 | self.timed_commands = [] 186 | self.content = None 187 | 188 | if obj: 189 | if isinstance(obj, IOBase): 190 | self.content = obj.read() 191 | else: 192 | self.content = obj 193 | 194 | self._parse() 195 | 196 | def _parse(self): 197 | for line in self.content.split('\n'): 198 | line = line.split('#')[0].strip() 199 | if not line or line.startswith('#'): 200 | continue 201 | 202 | for command in CommandList._split_input(line): 203 | self._parse_command(command) 204 | 205 | def _parse_command(self, command): 206 | args = CommandList._split_command_args(command) 207 | if not args: 208 | return 209 | 210 | _from, _to = CommandList.parse_time_range(args[0]) 211 | if _from != -1: 212 | self.add_command(TimedCommand(args[1:], _from, False)) 213 | 214 | if _to != -1: 215 | self.add_command(TimedCommand(args[1:], _to, True)) 216 | else: 217 | self.commands.append(Command(args)) 218 | 219 | def sorted_timed_commands(self) -> list: 220 | """ 221 | Returns all timed commands sorted in ascending order (stable). 222 | 223 | Returns: 224 | list: timed commands sorted in ascending order 225 | """ 226 | return sorted(self.timed_commands, key=lambda command: command.timestamp) 227 | 228 | def add_command(self, command: BaseCommand): 229 | """ 230 | Adds a command to the CommandList, converting it to an InputCommand if possible. 231 | 232 | The command will be added to the commands list if it is of type Command. 233 | If the command is a TimedCommand, it will first be attempted to convert it 234 | to an InputCommand. If the conversion fails, it is added without any conversions. 235 | If the command is an InputCommnad, it is added to the timed_commands list. 236 | 237 | Args: 238 | command (BaseCommand): the command to be added 239 | """ 240 | if type(command) == Command: 241 | self.commands.append(command) 242 | elif type(command) == TimedCommand: 243 | input_command = command.to_input_command() 244 | if input_command: 245 | self.timed_commands.append(input_command) 246 | else: 247 | self.timed_commands.append(command) 248 | elif type(command) == InputCommand: 249 | self.timed_commands.append(command) 250 | 251 | def to_script(self) -> str: 252 | """ 253 | Converts all immediate and timed commands to a valid TMInterface script. 254 | 255 | Returns: 256 | str: the string representing the TMInterface script, one command per line 257 | """ 258 | script = '' 259 | for command in self.commands: 260 | script += f'{command.to_script()}\n' 261 | 262 | for command in self.sorted_timed_commands(): 263 | script += f'{command.to_script()}\n' 264 | 265 | return script 266 | 267 | def clear(self): 268 | """ 269 | Clears all commands from the command list. 270 | """ 271 | self.commands.clear() 272 | self.timed_commands.clear() 273 | 274 | @staticmethod 275 | def _split_input(command_input: str) -> list: 276 | in_quotes = False 277 | commands = [] 278 | offset = 0 279 | for i, c in enumerate(command_input): 280 | if c == '\"': 281 | in_quotes = not in_quotes 282 | 283 | if not in_quotes and c == ';': 284 | commands.append(command_input[offset:i]) 285 | offset = i + 1 286 | 287 | if len(command_input) - offset > 0: 288 | commands.append(command_input[offset:]) 289 | 290 | return commands 291 | 292 | @staticmethod 293 | def _split_command_args(command: str) -> list: 294 | args = [] 295 | offset = 0 296 | i = 0 297 | while i < len(command): 298 | if command[i] == ' ': 299 | if command[offset] != ' ': 300 | args.append(command[offset:i]) 301 | 302 | offset = i + 1 303 | elif command[i] == '\"': 304 | i += 1 305 | closing = command.find('\"', i) 306 | if closing != -1: 307 | if closing - i > 0: 308 | args.append(command[i:closing]) 309 | 310 | i = closing 311 | offset = i + 1 312 | 313 | i += 1 314 | 315 | if len(command) - offset > 0: 316 | args.append(command[offset:]) 317 | 318 | return args 319 | 320 | @staticmethod 321 | def parse_time_range(range_str: str) -> tuple: 322 | """ 323 | Parses a time range. 324 | 325 | Parses a time range or a single timestamp, returning a tuple 326 | with two elements (from, to). 327 | 328 | If the parsed time range consists only of one timestamp, to is set to -1. 329 | If from > to, the two integers are swapped. 330 | 331 | Args: 332 | range_str (str): the time range to parse 333 | 334 | Returns: 335 | tuple: a tuple of two int's (from, to) 336 | """ 337 | timestamps = range_str.split('-', 1) 338 | timestamps_len = len(timestamps) 339 | if timestamps_len == 1: 340 | return CommandList.parse_time(timestamps[0]), -1 341 | elif timestamps_len == 2: 342 | _from = CommandList.parse_time(timestamps[0]) 343 | _to = CommandList.parse_time(timestamps[1]) 344 | if _from > _to: 345 | _from, _to = _to, _from 346 | 347 | return _from, _to 348 | 349 | return -1, -1 350 | 351 | @staticmethod 352 | def _parse_seconds(time_str: str) -> int: 353 | tokens = time_str.split('.', 1) 354 | if len(tokens) < 2: 355 | return -1 356 | 357 | if not tokens[0] or not tokens[1]: 358 | return -1 359 | 360 | tokens[1] = tokens[1][:2].ljust(2, '0') 361 | 362 | try: 363 | seconds = int(tokens[0]) 364 | milliseconds = int(tokens[1]) 365 | return seconds * 1000 + milliseconds * 10 366 | except ValueError: 367 | return -1 368 | 369 | @staticmethod 370 | def parse_time(time_str: str) -> int: 371 | """ 372 | Parses a singular timestamp which is either a number or a formatted time. 373 | 374 | Parses a string like "947120" or "15:47.12" to an integer time in milliseconds. 375 | 376 | Args: 377 | time_str (str): the time string to parse 378 | 379 | Returns: 380 | int: the time representing the string, -1 if parsing fails 381 | """ 382 | if '.' not in time_str: 383 | try: 384 | return int(time_str) 385 | except ValueError: 386 | return -1 387 | 388 | tokens = time_str.split(':', 2) 389 | if not tokens: 390 | return -1 391 | 392 | tokens_len = len(tokens) 393 | if tokens_len == 1: 394 | return CommandList._parse_seconds(time_str) 395 | elif tokens_len == 2: 396 | try: 397 | minutes = int(tokens[0]) 398 | except ValueError: 399 | return -1 400 | 401 | seconds = CommandList._parse_seconds(tokens[1]) 402 | if seconds == -1: 403 | return -1 404 | 405 | return minutes * 60000 + seconds 406 | elif tokens_len == 3: 407 | try: 408 | hours = int(tokens[0]) 409 | minutes = int(tokens[1]) 410 | except ValueError: 411 | return - 1 412 | 413 | seconds = CommandList._parse_seconds(tokens[2]) 414 | if seconds == -1: 415 | return -1 416 | 417 | return hours * 3600000 + minutes * 60000 + seconds 418 | 419 | return -1 420 | -------------------------------------------------------------------------------- /tminterface/constants.py: -------------------------------------------------------------------------------- 1 | TIMERS_SIZE = 212 2 | CMD_BUFFER_CORE_SIZE = 264 3 | DYNA_SIZE = 1424 4 | SCENE_MOBIL_SIZE = 2168 5 | SIMULATION_WHEELS_SIZE = 3056 6 | PLUG_SOLID_SIZE = 68 7 | PLAYER_INFO_SIZE = 952 8 | INPUT_STATE_SIZE = 120 9 | 10 | SIM_HAS_TIMERS = 0x1 11 | SIM_HAS_DYNA = 0x2 12 | SIM_HAS_SCENE_MOBIL = 0x4 13 | SIM_HAS_SIMULATION_WHEELS = 0x8 14 | SIM_HAS_PLUG_SOLID = 0x10 15 | SIM_HAS_CMD_BUFFER_CORE = 0x20 16 | SIM_HAS_INPUT_STATE = 0x40 17 | SIM_HAS_PLAYER_INFO = 0x80 18 | 19 | MODE_SIMULATION = 0 20 | MODE_RUN = 1 21 | 22 | BINARY_ACCELERATE_NAME = 'Accelerate' 23 | BINARY_BRAKE_NAME = 'Brake' 24 | BINARY_LEFT_NAME = 'Steer left' 25 | BINARY_RIGHT_NAME = 'Steer right' 26 | BINARY_RESPAWN_NAME = 'Respawn' 27 | BINARY_HORN_NAME = 'Horn' 28 | BINARY_RACE_START_NAME = '_FakeIsRaceRunning' 29 | BINARY_RACE_FINISH_NAME = '_FakeFinishLine' 30 | ANALOG_STEER_NAME = 'Steer (analog)' 31 | ANALOG_ACCELERATE_NAME = 'Accelerate (analog)' 32 | 33 | DEFAULT_SERVER_SIZE = 65535 34 | -------------------------------------------------------------------------------- /tminterface/eventbuffer.py: -------------------------------------------------------------------------------- 1 | import tminterface.util as util 2 | from tminterface.constants import ANALOG_ACCELERATE_NAME, ANALOG_STEER_NAME, BINARY_ACCELERATE_NAME, BINARY_BRAKE_NAME, BINARY_HORN_NAME, BINARY_LEFT_NAME, BINARY_RACE_FINISH_NAME, BINARY_RACE_START_NAME, BINARY_RESPAWN_NAME, BINARY_RIGHT_NAME 3 | from typing import Union 4 | from math import ceil 5 | from bytefield import ByteStruct, IntegerField 6 | 7 | 8 | class Event(ByteStruct): 9 | """ 10 | The Event class represents a game event (or input) with its respective time. 11 | 12 | Such event is stored in 8 bytes internally by the game. The first 4 bytes is the time 13 | of the event. This time is a stored time, which means it is offset by 100000ms. 14 | 15 | The last 4 bytes contain the event data. This data contains the actual event type 16 | (e.g. whether it was acceleration, braking, steering etc.) and the value of the event. 17 | 18 | The event type is 1 byte long and it signifes an index into an array of available event types. 19 | This array is variable based on the information contained within the replay file. As such, 20 | it is required to get index of the desired event type dynamically. You can easily get/set this property 21 | through the name_index accessors. 22 | 23 | The event value depends on the event type and is 3 bytes long. If the event type is binary 24 | (e.g. accelerate, brake, steer left), the value can be either 0 or 1. For managing this value type, 25 | use the binary_value getter/setter. 26 | 27 | If the event type is analog, the value is stored in a custom format. You can convert between this 28 | format and the format TMInterface uses by using util.data_to_analog_value and utils.analog_value_to_data. 29 | You can avoid using these functions simply by using the analog_value getter/setter. 30 | 31 | Args: 32 | time (int): the stored time of the event 33 | 34 | Parameters: 35 | time (int): the stored time of the event 36 | data (int): the final data that is written into game's memory 37 | """ 38 | time = IntegerField(offset=0) 39 | input_data = IntegerField() 40 | 41 | def __init__(self, *args, **kwargs) -> None: 42 | if len(args) == 1 and isinstance(args[0], int): 43 | super().__init__(**kwargs) 44 | self.time = args[0] 45 | elif len(args) == 2 and isinstance(args[0], int) and isinstance(args[1], int): 46 | super().__init__(**kwargs) 47 | self.time = args[0] 48 | self.input_data = args[1] 49 | else: 50 | super().__init__(*args, **kwargs) 51 | 52 | @property 53 | def name_index(self) -> int: 54 | return self.input_data >> 24 55 | 56 | @name_index.setter 57 | def name_index(self, index: int): 58 | self.input_data = self.input_data & 0xFFFFFF 59 | self.input_data = self.input_data | (index << 24) 60 | 61 | @property 62 | def binary_value(self) -> bool: 63 | return bool(self.input_data & 0xFFFFFF) 64 | 65 | @binary_value.setter 66 | def binary_value(self, value: bool): 67 | self.input_data = self.input_data & 0xFF000000 | int(value) 68 | 69 | @property 70 | def analog_value(self) -> int: 71 | return util.data_to_analog_value(self.input_data & 0xFFFFFF) 72 | 73 | @analog_value.setter 74 | def analog_value(self, value: int): 75 | self.input_data = self.input_data & 0xFF000000 | (util.analog_value_to_data(value) & 0xFFFFFF) 76 | 77 | 78 | class EventBufferData(object): 79 | """ 80 | The internal event buffer used to hold player inputs in run or simulation mode. 81 | 82 | While simulating a race, the game loads the inputs from a replay file 83 | into an internal buffer and begins to apply "events" (inputs) from this 84 | buffer. The buffer itself consists of 8 byte values, the first 4 bytes 85 | is used for the event time and the last 4 is used for event data. 86 | See the Event class for more information. 87 | 88 | The event time is so called a "stored" time. The stored time is 89 | defined as 100000 + race time. The stored time is saved in the 90 | replay file and is also used in the internal buffer itself. 91 | 92 | The buffer itself is stored in *decreasing* order. That means that the event 93 | at index 0 in the list is the last one simulated in the race. The start and end 94 | of the race is marked by special "_FakeIsRaceRunning" and "_FakeFinishLine" events. 95 | These events mark the start and finish of the race. Note that without the presence 96 | of "_FakeIsRaceRunning" event, the race will not start at all. This event has a 97 | constant stored time of 100000. 98 | 99 | Before the starting event, a "Respawn" event can be generated by the game, this 100 | event can also be saved in the replay file itself. The very first input that can be applied 101 | by the player happens at stored time of 100010. 102 | 103 | Arguments: 104 | events_duration (int): the duration of the events, equalling the finish time, mostly ignored and does not need to be set 105 | 106 | Parameters: 107 | events_duration (int): the duration of the events 108 | control_names (list): the list of supported event types by this buffer 109 | events (list): the list of events held by this buffer 110 | """ 111 | def __init__(self, events_duration: int): 112 | self.events_duration = events_duration 113 | self.control_names = [] 114 | self.events = [] 115 | 116 | def copy(self): 117 | """ 118 | Copies the event buffer with all its events. 119 | 120 | Returns: 121 | a deep copy of the original event buffer 122 | """ 123 | cpy = EventBufferData(self.events_duration) 124 | cpy.control_names = self.control_names[:] 125 | cpy.events = [Event(ev.time, ev.data) for ev in self.events] 126 | return cpy 127 | 128 | def clear(self): 129 | """ 130 | Removes all events in the current event buffer, leaving 131 | the race running event in the buffer. 132 | 133 | A race running event should always be present in the buffer, to 134 | make the game start the race. 135 | """ 136 | self.events = [] 137 | self.add(-10, '_FakeIsRaceRunning', True) 138 | 139 | def sort(self): 140 | """ 141 | Sorts the event buffer by time in decreasing order. 142 | 143 | This is the order that events are stored internally by the game. 144 | Calling this is not needed, if you are calling set_event_buffer. 145 | The server will always take care of properly sorting the events. 146 | """ 147 | self.events = sorted(self.events, key=lambda ev: ev.time, reverse=True) 148 | 149 | def add(self, time: int, event_name: str, value: Union[int, bool]): 150 | """ 151 | Adds an event to the event buffer. 152 | 153 | This is a wrapper function that provides easy API for adding new events. 154 | Depending on the event_name parameter, the method will interpret the value 155 | in different ways. If the event is an analog event, the value passed should 156 | be in the range of [-65536, 65536] where negative values represent left steering 157 | and postive, right steering. 158 | 159 | If the event is binary, the value should be False for disabling the input and 160 | True for enabling it. 161 | 162 | The time parameter is zero based, where 0 is the first human input that can be injected. 163 | Internally, 0 translates to stored time 100010, which is the first simulation step 164 | after the countdown. 165 | 166 | Args: 167 | time (int): zero based timestamp when the input is injected 168 | event_name (str): the event name that specifies the input type 169 | value (Union[int, bool]): the value for the event, based on the event type 170 | """ 171 | try: 172 | index = self.control_names.index(event_name) 173 | except ValueError: 174 | raise ValueError(f'Event name "{event_name}" does not exist in this event buffer') 175 | 176 | ev = Event(time + 100010) 177 | ev.name_index = index 178 | if event_name == ANALOG_ACCELERATE_NAME or event_name == ANALOG_STEER_NAME: 179 | ev.analog_value = value 180 | else: 181 | ev.binary_value = value 182 | 183 | self.events.append(ev) 184 | 185 | def find(self, **kwargs): 186 | """ 187 | Finds matching events according to keyword arguments. 188 | 189 | Any unspecified parameter will be skipped in the search and will not be compared. 190 | You may use this method to filter events based on time, event type and value. 191 | 192 | Find all analog steering events with value -65536: 193 | 194 | matching = event_buffer.find(event_name=structs.ANALOG_STEER_NAME, value=-65536) 195 | 196 | Find all events that happened at input time 0: 197 | 198 | matching = event_buffer.find(time=0) 199 | 200 | Find the finish line event: 201 | 202 | matching = event_buffer.find(event_name=structs.BINARY_RACE_FINISH_NAME, value=True) 203 | 204 | Calling this method without any keyword arguments will return all events in the buffer. 205 | 206 | Parameters: 207 | **kwargs: the keyword arguments 208 | 209 | Keyword Args: 210 | event_name (str): match events with this event type 211 | time (int): match events with this time (zero based) 212 | value (Union[int, bool]): match events with this value, bool if the event type is binary, int if analog, 213 | this parameter can only be filtered if event_name is provided 214 | 215 | Returns: 216 | list: the events that matched the query 217 | """ 218 | index = -1 219 | if 'event_name' in kwargs: 220 | try: 221 | index = self.control_names.index(kwargs['event_name']) 222 | except ValueError: 223 | raise ValueError(f'Event name "{kwargs["event_name"]}" does not exist in this event buffer') 224 | 225 | has_value = 'value' in kwargs 226 | has_time = 'time' in kwargs 227 | 228 | matched = [] 229 | for ev in reversed(self.events): 230 | if has_time and ev.time - 100010 != kwargs['time']: 231 | continue 232 | 233 | if index >= 0: 234 | if ev.name_index != index: 235 | continue 236 | 237 | if has_value: 238 | if kwargs['event_name'] == ANALOG_STEER_NAME or kwargs['event_name'] == ANALOG_ACCELERATE_NAME: 239 | if ev.analog_value != kwargs['value']: 240 | continue 241 | else: 242 | if ev.binary_value != kwargs['value']: 243 | continue 244 | 245 | matched.append(ev) 246 | 247 | return matched 248 | 249 | def to_commands_str(self, all_events=False): 250 | """ 251 | Converts event buffer events and constructs a string consisting 252 | of commands compatible with TMInterface's script syntax. 253 | 254 | By default, only events that happened in the race 255 | (signified by the race running and finish events) will be converted 256 | and appended to the final string. If you want to convert all commands 257 | that are available in the buffer, call this method with all_events set to True. 258 | 259 | Parameters: 260 | all_events (bool): whether to convert all commands available in the buffer 261 | 262 | Returns: 263 | str: the string containing commands compatible with TMInterface's script syntax 264 | """ 265 | ACTION_MAPPINGS = { 266 | BINARY_ACCELERATE_NAME: 'up', 267 | BINARY_BRAKE_NAME: 'down', 268 | BINARY_LEFT_NAME: 'left', 269 | BINARY_RIGHT_NAME: 'right', 270 | BINARY_RESPAWN_NAME: 'enter', 271 | BINARY_HORN_NAME: 'horn' 272 | } 273 | 274 | sorted_events = sorted(self.events, key=lambda ev: ev.time) 275 | 276 | commands = '' 277 | try: 278 | start_events = self.find(event_name=BINARY_RACE_START_NAME) 279 | if start_events: 280 | start_time = start_events[0].time 281 | else: 282 | start_time = 100000 283 | except ValueError: 284 | start_time = 100000 285 | 286 | for ev in sorted_events: 287 | event_name = self.control_names[ev.name_index] 288 | if not all_events: 289 | if ev.time < start_time: 290 | continue 291 | 292 | if event_name == BINARY_RACE_FINISH_NAME: 293 | break 294 | 295 | if event_name in [BINARY_RESPAWN_NAME, BINARY_HORN_NAME] and not ev.binary_value: 296 | continue 297 | 298 | time = int(ceil((ev.time - start_time - 10) / 10) * 10) 299 | if event_name in ACTION_MAPPINGS.keys(): 300 | if ev.binary_value: 301 | commands += f'{time} press {ACTION_MAPPINGS[event_name]}\n' 302 | else: 303 | commands += f'{time} rel {ACTION_MAPPINGS[event_name]}\n' 304 | 305 | elif event_name == ANALOG_ACCELERATE_NAME: 306 | commands += f'{time} gas {ev.analog_value}\n' 307 | elif event_name == ANALOG_STEER_NAME: 308 | commands += f'{time} steer {ev.analog_value}\n' 309 | 310 | return commands 311 | -------------------------------------------------------------------------------- /tminterface/interface.py: -------------------------------------------------------------------------------- 1 | import struct 2 | import threading 3 | import time 4 | import mmap 5 | from ctypes import windll, c_char_p 6 | from typing import Tuple 7 | 8 | from tminterface.client import Client 9 | from tminterface.structs import BFEvaluationResponse, BFEvaluationInfo, ClassicString, CheckpointData, SimStateData 10 | from tminterface.eventbuffer import EventBufferData, Event 11 | from tminterface.constants import * 12 | from enum import IntEnum, auto 13 | 14 | 15 | class MessageType(IntEnum): 16 | S_RESPONSE = auto() 17 | S_ON_REGISTERED = auto() 18 | S_SHUTDOWN = auto() 19 | S_ON_RUN_STEP = auto() 20 | S_ON_SIM_BEGIN = auto() 21 | S_ON_SIM_STEP = auto() 22 | S_ON_SIM_END = auto() 23 | S_ON_CHECKPOINT_COUNT_CHANGED = auto() 24 | S_ON_LAPS_COUNT_CHANGED = auto() 25 | S_ON_CUSTOM_COMMAND = auto() 26 | S_ON_BRUTEFORCE_EVALUATE = auto() 27 | C_REGISTER = auto() 28 | C_DEREGISTER = auto() 29 | C_PROCESSED_CALL = auto() 30 | C_SET_INPUT_STATES = auto() 31 | C_RESPAWN = auto() 32 | C_GIVE_UP = auto() 33 | C_HORN = auto() 34 | C_SIM_REWIND_TO_STATE = auto() 35 | C_SIM_GET_STATE = auto() 36 | C_SIM_GET_EVENT_BUFFER = auto() 37 | C_GET_CONTEXT_MODE = auto() 38 | C_SIM_SET_EVENT_BUFFER = auto() 39 | C_SIM_SET_TIME_LIMIT = auto() 40 | C_GET_CHECKPOINT_STATE = auto() 41 | C_SET_CHECKPOINT_STATE = auto() 42 | C_SET_GAME_SPEED = auto() 43 | C_EXECUTE_COMMAND = auto() 44 | C_SET_EXECUTE_COMMANDS = auto() 45 | C_SET_TIMEOUT = auto() 46 | C_REMOVE_STATE_VALIDATION = auto() 47 | C_PREVENT_SIMULATION_FINISH = auto() 48 | C_REGISTER_CUSTOM_COMMAND = auto() 49 | C_LOG = auto() 50 | ANY = auto() 51 | 52 | 53 | RESPONSE_TOO_LONG = 1 54 | CLIENT_ALREADY_REGISTERED = 2 55 | NO_EVENT_BUFFER = 3 56 | NO_PLAYER_INFO = 4 57 | COMMAND_ALREADY_REGISTERED = 5 58 | 59 | MAXINT32 = 2 ** 31 - 1 60 | 61 | 62 | class Message(object): 63 | """ 64 | The Message class represents a binary buffer that contains useful methods to construct 65 | a message to send to the server. A message additionally contains its type, whether it is 66 | a response to a server call, or a normal client call. It also contains an error code, 67 | if there was any failure writing the message. 68 | 69 | Args: 70 | _type (int): the message type 71 | error_code (int): the error code of the message, 0 if none 72 | 73 | Parameters: 74 | _type (int): the message type 75 | error_code (int): the error code of the message, 0 if none 76 | data (bytearray): the binary data 77 | """ 78 | def __init__(self, _type: int, error_code=0): 79 | self._type = _type 80 | self.error_code = error_code 81 | self.data = bytearray() 82 | 83 | def write_uint8(self, n): 84 | self.data.extend(struct.pack('B', n)) 85 | 86 | def write_int16(self, n: int): 87 | self.data.extend(struct.pack('h', n)) 88 | 89 | def write_uint16(self, n: int): 90 | self.data.extend(struct.pack('H', n)) 91 | 92 | def write_int32(self, n: int): 93 | self.data.extend(struct.pack('i', n)) 94 | 95 | def write_uint32(self, n: int): 96 | self.data.extend(struct.pack('I', n)) 97 | 98 | def write_double(self, n: float): 99 | self.data.extend(struct.pack('d', n)) 100 | 101 | def write_buffer(self, buffer: bytearray): 102 | self.data.extend(buffer) 103 | 104 | def write_zeros(self, n_bytes): 105 | self.data.extend(bytearray(n_bytes)) 106 | 107 | def write_int(self, n, size): 108 | if size == 1: 109 | self.write_uint8(n) 110 | elif size == 2: 111 | if n < 0: 112 | self.write_int16(n) 113 | else: 114 | self.write_uint16(n) 115 | elif size == 4: 116 | if n == 0xffffffff: 117 | self.write_uint32(n) 118 | else: 119 | self.write_int32(n) 120 | 121 | def to_data(self) -> bytearray: 122 | return bytearray(struct.pack('i', self._type)) + bytearray(struct.pack('i', self.error_code)) + self.data 123 | 124 | def __len__(self): 125 | return 8 + len(self.data) 126 | 127 | 128 | class ServerException(Exception): 129 | """ 130 | An exception thrown when the server cannot perform requested operation. 131 | """ 132 | pass 133 | 134 | 135 | class TMInterface(object): 136 | """ 137 | TMInterface is the main class to communicate with the TMInterface server. 138 | The communication is done through memory mapping and a simple synchronous 139 | message system between the server and the client. A TMInterface server 140 | can only serve only one client at a time, it is however possible to connect 141 | to many servers from the same script. 142 | 143 | The interface provides various functions to manipulate game state 144 | and hook to different periods of game execution. 145 | 146 | Args: 147 | server_name (str): the server tag to connect to 148 | buffer_size (int): the buffer size used by the server, the default is 149 | specified by tminterface.constants.DEFAULT_BUFFER_SIZE. 150 | Using a custom size requires launching TMInterface with the 151 | /serversize command line parameter: TMInterface.exe /serversize=size. 152 | 153 | Parameters: 154 | server_name (str): the server tag that's used 155 | running (bool): whether the client is running or not 156 | registered (bool): whether the client is registered 157 | mfile (mmap.mmap): the internal mapped file used for communication 158 | buffer_size (int): the buffer size used for communication 159 | client (Client): the registered client that's controlling the server 160 | """ 161 | def __init__(self, server_name='TMInterface0', buffer_size=DEFAULT_SERVER_SIZE): 162 | self.server_name = server_name 163 | self.running = True 164 | self.registered = False 165 | self.mfile = None 166 | self.buffer_size = buffer_size 167 | self.client = None 168 | self.empty_buffer = bytearray(self.buffer_size) 169 | self.thread = None 170 | self.request_close = False 171 | 172 | def register(self, client: Client) -> bool: 173 | """ 174 | Registers a client on the server. 175 | The server can only register one client at a time, if the client is already 176 | registered, the method will return False. 177 | 178 | This method will initially start a new thread and send a message to the server 179 | to register a new client. After a successful registration, :meth:`Client.on_registered` 180 | will be called with the instance of the TMInterface class. 181 | 182 | Args: 183 | client (Client): a Client instance to register 184 | 185 | Returns: 186 | True if registration was scheduled, False if client is already registered 187 | """ 188 | if self.client is not None: 189 | return False 190 | 191 | if self.registered: 192 | return False 193 | 194 | self.registered = False 195 | self.client = client 196 | 197 | if self.thread is None: 198 | self.thread = threading.Thread(target=self._main_thread) 199 | self.thread.daemon = True 200 | self.thread.start() 201 | 202 | return True 203 | 204 | def close(self): 205 | """ 206 | Closes the connection to the server by deregistering the current client 207 | and shutting down the thread for communication. 208 | 209 | This method will send a message to the server 210 | to deregister the current client. 211 | 212 | After a successful deregistration, :meth:`Client.on_deregistered` 213 | will be called with the instance of the TMInterface class. 214 | """ 215 | if self.registered: 216 | msg = Message(MessageType.C_DEREGISTER) 217 | msg.write_int32(0) 218 | self._send_message(msg) 219 | self.client.on_deregistered(self) 220 | self.thread = None 221 | 222 | self.running = False 223 | 224 | def set_timeout(self, timeout_ms: int): 225 | """ 226 | Sets the timeout window in which the client has to respond to server calls. 227 | 228 | The timeout specifies how long will the server wait for a response from the client. 229 | If the response does not arrive in this time frame, the server will automatically deregister the 230 | client itself. 231 | 232 | The timeout is specified in milliseconds, by default this is 2000ms (2 seconds). Set the timeout 233 | to -1 to have the server wait forever for the response. 234 | 235 | Args: 236 | timeout_ms (int): the timeout in milliseconds 237 | """ 238 | msg = Message(MessageType.C_SET_TIMEOUT) 239 | msg.write_int32(timeout_ms) 240 | self._send_message(msg) 241 | self._wait_for_server_response() 242 | 243 | def set_speed(self, speed: float): 244 | """ 245 | Sets the global game speed, internally this simply sets the console variable 246 | "speed" in an TMInterface instance. 247 | 248 | All characteristics of setting the global speed apply. It is not recommended 249 | to set the speed to high factors (such as >100), which could cause the game 250 | to skip running some subsystems such as the input subsystem. 251 | 252 | This variable does not affect simulation contexts in which debug mode is disabled. 253 | When debug mode is disabled (default), the game runs only the simulation subsystem. 254 | 255 | Args: 256 | speed (float): the speed to set, 1 is the default normal game speed, 257 | factors <1 will slow down the game while factors >1 will speed it up 258 | """ 259 | msg = Message(MessageType.C_SET_GAME_SPEED) 260 | msg.write_double(speed) 261 | self._send_message(msg) 262 | self._wait_for_server_response() 263 | 264 | def set_input_state(self, sim_clear_buffer: bool = True, **kwargs): 265 | """ 266 | Sets the game input state of the vehicle. 267 | 268 | Sets individual input states for the car. If successfully applied, 269 | key states are guaranteed to be applied at next physics tick. 270 | If you want to apply an input state that happens at 500ms, call 271 | send this message at 490ms (one step before). 272 | 273 | Note that it is not guaranteed that the game will actually process the input 274 | in the RUN mode. This can happen when setting the game speed to high factors 275 | (such as >100). This does not affect the simulation context. 276 | 277 | In a simulation context, the server will add new input events to the existing 278 | event buffer such that that the next tick has the desired input state. By default, 279 | all other input events are cleared. If you want to preserve existing input state & events, 280 | pass sim_clear_buffer=False. 281 | 282 | Arguments left, right, accelerate and brake are binary events. 283 | To disable an action pass False and to enable it, pass True. 284 | 285 | Arguments steer and gas are analog events. Pass a value in the range of [-65536, 65536] to modify 286 | the state of these actions. You can also use the extended steer range of [-6553600, 6553600], 287 | note however that this range is not possible to achieve on physical hardware. This call 288 | is not affected by the extended_steer console variable. 289 | 290 | Args: 291 | sim_clear_buffer (bool): whether to clear the event buffer when setting 292 | input state in simulation 293 | **kwargs: the keyword arguments 294 | 295 | Keyword Args: 296 | left (bool): the left binary input, False = disabled, True = enabled 297 | right (bool): the right binary input, False = disabled, True = enabled 298 | accelerate (bool): the up binary input, False = disabled, True = enabled 299 | brake (bool): the down binary input, False = disabled, True = enabled 300 | steer (int): the steer analog input, in range of [-65536, 65536] 301 | gas (int): the gas analog input, in range of [-65536, 65536] 302 | """ 303 | if self.get_context_mode() == MODE_SIMULATION and sim_clear_buffer: 304 | self.clear_event_buffer() 305 | 306 | msg = Message(MessageType.C_SET_INPUT_STATES) 307 | if 'left' in kwargs: 308 | msg.write_int32(int(kwargs['left'])) 309 | else: 310 | msg.write_int32(-1) 311 | 312 | if 'right' in kwargs: 313 | msg.write_int32(int(kwargs['right'])) 314 | else: 315 | msg.write_int32(-1) 316 | 317 | if 'accelerate' in kwargs: 318 | msg.write_int32(int(kwargs['accelerate'])) 319 | else: 320 | msg.write_int32(-1) 321 | 322 | if 'brake' in kwargs: 323 | msg.write_int32(int(kwargs['brake'])) 324 | else: 325 | msg.write_int32(-1) 326 | 327 | if 'steer' in kwargs: 328 | msg.write_int32(kwargs['steer']) 329 | else: 330 | msg.write_int32(MAXINT32) 331 | 332 | if 'gas' in kwargs: 333 | msg.write_int32(kwargs['gas']) 334 | else: 335 | msg.write_int32(MAXINT32) 336 | 337 | self._send_message(msg) 338 | self._wait_for_server_response() 339 | 340 | def respawn(self, sim_clear_events: bool = True): 341 | """ 342 | Queues a deterministic respawn at the next race tick. This function 343 | will not immediately call the game to respawn the car, as TMInterface 344 | has to call the specific function at a specific place in the game loop. 345 | 346 | In a simulation context, the server will add a new input event to the existing 347 | event buffer such that that the car respawns at the next tick. By default, 348 | all other input events are cleared. If you want to preserve existing input events, 349 | pass sim_clear_events=False. 350 | 351 | The function will respawn the car to the nearest respawnable checkpoint or 352 | if there was no passed checkpoints, restart the race. The behaviour of this function 353 | also depends on the start_respawn console variable set within TMInterface. 354 | If start_respawn is set to true, respawning without any passed checkpoints will 355 | not restart the race, but only respawn the car on the start block, simulating 356 | online respawn behaviour. 357 | 358 | Args: 359 | sim_clear_events (bool): whether to clear all other events in simulation mode 360 | """ 361 | if self.get_context_mode() == MODE_SIMULATION and sim_clear_events: 362 | self.clear_event_buffer() 363 | 364 | msg = Message(MessageType.C_RESPAWN) 365 | msg.write_int32(0) 366 | self._send_message(msg) 367 | self._wait_for_server_response() 368 | 369 | def give_up(self): 370 | """ 371 | Restarts the current race. 372 | 373 | This function does not do anything in a simulation context. 374 | To rewind to the start of the race in the simulation context, use simulation states. 375 | """ 376 | msg = Message(MessageType.C_GIVE_UP) 377 | msg.write_int32(0) 378 | self._send_message(msg) 379 | self._wait_for_server_response() 380 | 381 | def horn(self, sim_clear_events: bool = True): 382 | """ 383 | Queues a deterministic horn at next race tick. This function 384 | will not immediately call the game to horn, as TMInterface 385 | has to call the specific function at a specific place in the game loop. 386 | 387 | In a simulation context, the server will add a new input event to the existing 388 | event buffer such that that the car horns at the next tick. By default, 389 | all other input events are cleared. If you want to preserve existing input events, 390 | pass sim_clear_events=False. 391 | 392 | Args: 393 | sim_clear_events (bool): whether to clear all other events in simulation mode 394 | """ 395 | if self.get_context_mode() == MODE_SIMULATION and sim_clear_events: 396 | self.clear_event_buffer() 397 | 398 | msg = Message(MessageType.C_HORN) 399 | msg.write_int32(0) 400 | self._send_message(msg) 401 | self._wait_for_server_response() 402 | 403 | def execute_command(self, command: str): 404 | """ 405 | Adds an interface command to the internal command queue. 406 | 407 | The command will not be immediately executed, rather, it may be executed when 408 | the current queue is processed on the next game frame. 409 | 410 | Args: 411 | command (str): the command to execute 412 | """ 413 | command_str = ClassicString(command) 414 | 415 | msg = Message(MessageType.C_EXECUTE_COMMAND) 416 | msg.write_int32(0) 417 | msg.write_buffer(command_str.data) 418 | self._send_message(msg) 419 | self._wait_for_server_response() 420 | 421 | def remove_state_validation(self): 422 | """ 423 | Makes the game validate the replay without checking if the inputs match 424 | the states saved in the replay, as if it was validating a replay exported 425 | for validation. 426 | 427 | Calling this method in the on_simulation_begin call will remove state 428 | validation from currently validated replay. After calling, TrackMania will not 429 | check if the simulation matches with saved states in the replay, 430 | therefore allowing for input modification without stopping 431 | the simulation prematurely. 432 | """ 433 | msg = Message(MessageType.C_REMOVE_STATE_VALIDATION) 434 | msg.write_int32(0) 435 | self._send_message(msg) 436 | self._wait_for_server_response() 437 | 438 | def prevent_simulation_finish(self): 439 | """ 440 | Prevents the game from stopping the simulation after a finished race. 441 | 442 | Calling this method in the on_checkpoint_count_changed will invalidate 443 | checkpoint state so that the game does not stop simulating the race. 444 | Internally this is simply setting the last checkpoint time to -1 445 | and can be also done manually in the client if additional handling 446 | is required. 447 | """ 448 | msg = Message(MessageType.C_PREVENT_SIMULATION_FINISH) 449 | msg.write_int32(0) 450 | self._send_message(msg) 451 | self._wait_for_server_response() 452 | 453 | def rewind_to_state(self, state: SimStateData): 454 | """ 455 | Rewinds to the provided simulation state. 456 | 457 | The method of restoring the simulation state slightly varies depending 458 | on the context_mode field of the SimStateData class. Some buffers 459 | may not be restored at all but are replaced with native game function calls. 460 | 461 | The simulation state is obtainable through the get_simulation_state method. 462 | This state can also be used to write a save state compatible file for TMInterface. 463 | Note that modifying important parts of the state invalidates the current race. 464 | 465 | To provide state restoration across game instances, TMInterface uses 466 | memory masks to omit restoring instance specific fields such as pointers 467 | or arrays. 468 | 469 | The method can be called in on_run_step or on_simulation_step calls. 470 | Note that rewinding to a state in any of these hooks will immediately 471 | simulate the next step after the hook. For example, rewinding to a state 472 | saved at race time 0, will result in the next call to on_run_step/on_simulation_step 473 | being at time 10. If you want to apply any immediate input state, 474 | make sure to apply it in the same physics step as the call to rewind_to_state. 475 | 476 | Args: 477 | state (SimStateData): the state to restore, obtained through get_simulation_state 478 | """ 479 | msg = Message(MessageType.C_SIM_REWIND_TO_STATE) 480 | msg.write_buffer(state.data) 481 | self._send_message(msg) 482 | 483 | # Send client the number of CPs of the state rewinded to 484 | cp_count = len([cp_time.time for cp_time in state.cp_data.cp_times if cp_time.time != -1]) 485 | cp_target = len(state.cp_data.cp_times) 486 | self.client.on_checkpoint_count_changed(self, cp_count, cp_target) 487 | 488 | # Send client the number of laps of the state rewinded to 489 | if (len(state.cp_data.cp_states) > 0): 490 | lap_count = cp_count // len(state.cp_data.cp_states) 491 | self.client.on_laps_count_changed(self, lap_count) 492 | 493 | self._wait_for_server_response() 494 | 495 | def set_checkpoint_state(self, data: CheckpointData): 496 | """ 497 | Sets the checkpoint state of the game. 498 | See get_checkpoint_state to learn more about how the game stores checkpoint information. 499 | 500 | Args: 501 | data (CheckpointData): the checkpoint data 502 | """ 503 | msg = Message(MessageType.C_SET_CHECKPOINT_STATE) 504 | msg.write_buffer(data.data) 505 | self._send_message(msg) 506 | self._wait_for_server_response() 507 | 508 | def set_event_buffer(self, data: EventBufferData): 509 | """ 510 | Replaces the internal event buffer used for simulation with a new one. 511 | 512 | If you do not modify existing inputs or do not generate all events 513 | beforehand, use TMInterface.set_input_state for dynamic input injection. 514 | See EventBufferData for more information. 515 | 516 | The events_duration and control_names fields are ignored in this call. 517 | 518 | Args: 519 | data (EventBufferData): the new event buffer 520 | """ 521 | msg = Message(MessageType.C_SIM_SET_EVENT_BUFFER) 522 | for _ in range(10): 523 | msg.write_int32(-1) 524 | 525 | msg.write_int32(data.events_duration) 526 | msg.write_uint32(len(data.events)) 527 | for event in data.events: 528 | msg.write_buffer(event.data) 529 | 530 | self._send_message(msg) 531 | self._wait_for_server_response() 532 | 533 | def get_context_mode(self) -> int: 534 | """ 535 | Gets the context mode the TMInterface instance is currently in. 536 | 537 | The context mode is determining if the current race is in 538 | "run" mode, that is a normal race or "simulation" mode, which is when 539 | a player validates a replay. 540 | 541 | Returns: 542 | int: MODE_SIMULATION (0) if the player is in the simulation mode, MODE_RUN (1) if in a normal race 543 | """ 544 | msg = Message(MessageType.C_GET_CONTEXT_MODE) 545 | self._send_message(msg) 546 | self._wait_for_server_response(False) 547 | 548 | self.mfile.seek(8) 549 | mode = self._read_int32() 550 | self._clear_buffer() 551 | return mode 552 | 553 | def get_checkpoint_state(self) -> CheckpointData: 554 | """ 555 | Gets the current checkpoint state of the race. 556 | 557 | See CheckpointData for more information. 558 | 559 | Returns: 560 | CheckpointData: the object that holds the two arrays representing checkpoint state 561 | """ 562 | msg = Message(MessageType.C_GET_CHECKPOINT_STATE) 563 | self._send_message(msg) 564 | self._wait_for_server_response(False) 565 | 566 | self.mfile.seek(4) 567 | error_code = self._read_int32() 568 | if error_code == NO_PLAYER_INFO: 569 | raise ServerException('Failed to get checkpoint state: no player info available') 570 | 571 | data = CheckpointData(self.mfile.read(CheckpointData.min_size)) 572 | 573 | self._clear_buffer() 574 | return data 575 | 576 | def get_simulation_state(self) -> SimStateData: 577 | """ 578 | Gets the current simulation state of the race. 579 | 580 | The method can be called in on_run_step or on_simulation_step calls. 581 | See SimStateData for more information. 582 | 583 | Returns: 584 | SimStateData: the object holding the simulation state 585 | """ 586 | msg = Message(MessageType.C_SIM_GET_STATE) 587 | self._send_message(msg) 588 | self._wait_for_server_response(False) 589 | 590 | self.mfile.seek(4) 591 | error_code = self._read_int32() 592 | 593 | state = SimStateData(self.mfile.read(SimStateData.min_size)) 594 | state.cp_data.read_from_file(self.mfile) 595 | 596 | if error_code == NO_PLAYER_INFO: 597 | raise ServerException('Failed to get checkpoint state: no player info available') 598 | 599 | self._clear_buffer() 600 | return state 601 | 602 | def get_event_buffer(self) -> EventBufferData: 603 | """ 604 | Gets the internal event buffer used to hold player inputs in run or simulation mode. 605 | If the server is in the run mode (that is, in a normal race controlled by the player), 606 | this method returns the inputs of the current race. Note that new inputs will be added 607 | to the buffer as the player or TMInterface injects inputs into the game. 608 | 609 | See EventBufferData for more information. 610 | 611 | Returns: 612 | EventBufferData: the event buffer holding all the inputs of the current simulation 613 | """ 614 | msg = Message(MessageType.C_SIM_GET_EVENT_BUFFER) 615 | self._send_message(msg) 616 | self._wait_for_server_response(False) 617 | 618 | self.mfile.seek(4) 619 | error_code = self._read_uint32() 620 | if error_code == NO_EVENT_BUFFER: 621 | raise ServerException('Failed to get event buffer: no event buffer available') 622 | 623 | names = [None] * 10 624 | _id = self._read_int32() 625 | if _id != -1: 626 | names[_id] = BINARY_RACE_START_NAME 627 | 628 | _id = self._read_int32() 629 | if _id != -1: 630 | names[_id] = BINARY_RACE_FINISH_NAME 631 | 632 | _id = self._read_int32() 633 | if _id != -1: 634 | names[_id] = BINARY_ACCELERATE_NAME 635 | 636 | _id = self._read_int32() 637 | if _id != -1: 638 | names[_id] = BINARY_BRAKE_NAME 639 | 640 | _id = self._read_int32() 641 | if _id != -1: 642 | names[_id] = BINARY_LEFT_NAME 643 | 644 | _id = self._read_int32() 645 | if _id != -1: 646 | names[_id] = BINARY_RIGHT_NAME 647 | 648 | _id = self._read_int32() 649 | if _id != -1: 650 | names[_id] = ANALOG_STEER_NAME 651 | 652 | _id = self._read_int32() 653 | if _id != -1: 654 | names[_id] = ANALOG_ACCELERATE_NAME 655 | 656 | _id = self._read_int32() 657 | if _id != -1: 658 | names[_id] = BINARY_RESPAWN_NAME 659 | 660 | _id = self._read_int32() 661 | if _id != -1: 662 | names[_id] = BINARY_HORN_NAME 663 | 664 | data = EventBufferData(self._read_uint32()) 665 | data.control_names = names 666 | events = self._read_uint32() 667 | for _ in range(events): 668 | ev = Event(self.mfile.read(Event.min_size)) 669 | data.events.append(ev) 670 | 671 | self._clear_buffer() 672 | return data 673 | 674 | def clear_event_buffer(self): 675 | """ 676 | Clears the current event buffer used for simulation, leaving 677 | the race running event in the buffer. 678 | 679 | A race running event should always be present in the buffer, to 680 | make the game start the race. 681 | """ 682 | event_buffer = self.get_event_buffer() 683 | event_buffer.clear() 684 | self.set_event_buffer(event_buffer) 685 | 686 | def set_simulation_time_limit(self, time: int): 687 | """ 688 | Sets the time limit of the simulation. 689 | 690 | This allows for setting an arbitrary time limit for the running 691 | simulation, making the game stop the simulation after the provided 692 | time limit is exhausted. 693 | 694 | By default, this limit is set to the finish time of the original replay 695 | (taken from events duration found in the events buffer). 696 | 697 | Note that setting the limit to a large value will extend the simulation 698 | to that limit, even after the race is finished. Make sure to manage 699 | finishing the race according to your application (e.g by rewinding to a state). 700 | 701 | To reset the time to the original limit, pass -1. 702 | This call applies only to the simulation context. 703 | 704 | Args: 705 | time (int): the time at which the game stops simulating, pass -1 to reset 706 | to the original value 707 | """ 708 | msg = Message(MessageType.C_SIM_SET_TIME_LIMIT) 709 | msg.write_int32(time) 710 | self._send_message(msg) 711 | self._wait_for_server_response() 712 | 713 | def register_custom_command(self, command: str): 714 | """ 715 | Registers a custom command within the console. 716 | 717 | This function allows you to implement a custom command that is registered within TMInterface's console. 718 | When executing the command, the :meth:`Client.on_custom_command` method will be called with additional 719 | arguments such as the time range and processed arguments list. 720 | 721 | It is completely up to the command implementation to process the time range and additional 722 | arguments supplied in the on_custom_command hook. Quoted arguments such as filenames will 723 | be automatically joined into one argument, even if they contain spaces. 724 | 725 | A console command is not immediately executed after submitting it to the console. 726 | TMInterface executes commands asynchronously, processing a fixed amount of commands 727 | each frame. This is done to prevent the game from hanging when loading scripts with 728 | 1000's of commands. 729 | 730 | Use the log() method to output any info about the execution of your command. 731 | 732 | Args: 733 | command (str): the command to register, the command cannot contain spaces 734 | """ 735 | str = ClassicString(command) 736 | 737 | msg = Message(MessageType.C_REGISTER_CUSTOM_COMMAND) 738 | msg.write_int32(0) 739 | msg.write_buffer(str.data) 740 | self._send_message(msg) 741 | self._wait_for_server_response(False) 742 | 743 | self.mfile.seek(4) 744 | error_code = self._read_int32() 745 | if error_code == COMMAND_ALREADY_REGISTERED: 746 | raise ServerException(f'Failed to register custom command: {command} is already registered') 747 | 748 | self._clear_buffer() 749 | 750 | def log(self, message: str, severity='log'): 751 | """ 752 | Prints a message in TMInterface's console. 753 | 754 | You can specify the severity of the command to highlight the line in a different color. 755 | 756 | Args: 757 | message (str): the message to print 758 | severity (str): one of: "log", "success", "warning", "error", the message severity 759 | """ 760 | severity_id = 0 761 | if severity == 'success': 762 | severity_id = 1 763 | elif severity == 'warning': 764 | severity_id = 2 765 | elif severity == 'error': 766 | severity_id = 3 767 | 768 | msg = Message(MessageType.C_LOG) 769 | msg.write_int32(severity_id) 770 | self._write_vector(msg, [ord(c) for c in message], 1) 771 | self._send_message(msg) 772 | self._wait_for_server_response() 773 | 774 | def _on_bruteforce_validate_call(self, msgtype: MessageType): 775 | info = BFEvaluationInfo(self.mfile.read(BFEvaluationInfo.min_size)) 776 | 777 | resp = self.client.on_bruteforce_evaluate(self, info) 778 | if not resp: 779 | resp = BFEvaluationResponse() 780 | 781 | msg = Message(MessageType.C_PROCESSED_CALL) 782 | msg.write_int32(msgtype) 783 | msg.write_buffer(resp.data) 784 | self._send_message(msg) 785 | 786 | def _write_vector(self, msg: Message, vector: list, field_sizes): 787 | is_list = isinstance(field_sizes, list) 788 | if is_list: 789 | item_size = sum(field_sizes) 790 | else: 791 | item_size = field_sizes 792 | 793 | if len(msg) + 4 > self.buffer_size: 794 | return False 795 | 796 | vsize = len(vector) 797 | msgsize = len(msg) + 4 + item_size * vsize 798 | if msgsize > self.buffer_size: 799 | msg.write_int32(0) 800 | msg.error_code = RESPONSE_TOO_LONG 801 | return True 802 | 803 | msg.write_int32(vsize) 804 | if is_list: 805 | for elem in vector: 806 | for i, field in enumerate(elem): 807 | msg.write_int(field, field_sizes[i]) 808 | else: 809 | for elem in vector: 810 | msg.write_int(elem, field_sizes) 811 | 812 | return True 813 | 814 | def __read_vector(self, field_sizes: Tuple[int, list]) -> list: 815 | if self.mfile.tell() + 4 > self.buffer_size: 816 | return [] 817 | 818 | size = self._read_int32() 819 | vec = [] 820 | for _ in range(size): 821 | if isinstance(field_sizes, list): 822 | tup = [] 823 | for size in field_sizes: 824 | tup.append(self._read_int(size)) 825 | 826 | vec.append(tuple(tup)) 827 | else: 828 | vec.append(self._read_int(field_sizes)) 829 | 830 | return vec 831 | 832 | def _main_thread(self): 833 | while self.running: 834 | if not self._ensure_connected(): 835 | time.sleep(0) 836 | continue 837 | 838 | if not self.registered: 839 | msg = Message(MessageType.C_REGISTER) 840 | self._send_message(msg) 841 | self._wait_for_server_response() 842 | self.registered = True 843 | 844 | self._process_server_message() 845 | time.sleep(0) 846 | 847 | def _process_server_message(self): 848 | if self.mfile is None: 849 | return 850 | 851 | self.mfile.seek(0) 852 | msgtype = self._read_int32() 853 | if msgtype & 0xFF00 == 0: 854 | return 855 | 856 | msgtype &= 0xFF 857 | 858 | # error_code = self.__read_int32() 859 | self._skip(4) 860 | 861 | if msgtype == MessageType.S_SHUTDOWN: 862 | self.close() 863 | self.client.on_shutdown(self) 864 | elif msgtype == MessageType.S_ON_RUN_STEP: 865 | _time = self._read_int32() 866 | self.client.on_run_step(self, _time) 867 | self._respond_to_call(msgtype) 868 | elif msgtype == MessageType.S_ON_SIM_BEGIN: 869 | self.client.on_simulation_begin(self) 870 | self._respond_to_call(msgtype) 871 | elif msgtype == MessageType.S_ON_SIM_STEP: 872 | _time = self._read_int32() 873 | self.client.on_simulation_step(self, _time) 874 | self._respond_to_call(msgtype) 875 | elif msgtype == MessageType.S_ON_SIM_END: 876 | result = self._read_int32() 877 | self.client.on_simulation_end(self, result) 878 | self._respond_to_call(msgtype) 879 | elif msgtype == MessageType.S_ON_CHECKPOINT_COUNT_CHANGED: 880 | current = self._read_int32() 881 | target = self._read_int32() 882 | self.client.on_checkpoint_count_changed(self, current, target) 883 | self._respond_to_call(msgtype) 884 | elif msgtype == MessageType.S_ON_LAPS_COUNT_CHANGED: 885 | current = self._read_int32() 886 | self.client.on_laps_count_changed(self, current) 887 | self._respond_to_call(msgtype) 888 | elif msgtype == MessageType.S_ON_BRUTEFORCE_EVALUATE: 889 | self._on_bruteforce_validate_call(msgtype) 890 | elif msgtype == MessageType.S_ON_REGISTERED: 891 | self.registered = True 892 | self.client.on_registered(self) 893 | self._respond_to_call(msgtype) 894 | elif msgtype == MessageType.S_ON_CUSTOM_COMMAND: 895 | _from = self._read_int32() 896 | to = self._read_int32() 897 | n_args = self._read_int32() 898 | command = self._read_string() 899 | args = [] 900 | for _ in range(n_args): 901 | args.append(self._read_string()) 902 | 903 | self.client.on_custom_command(self, _from, to, command, args) 904 | self._respond_to_call(msgtype) 905 | 906 | def _is_mapped_file_present(self): 907 | FILE_MAP_ALL_ACCESS = 0xF001F 908 | inherit_handle = 0 909 | HANDLE_NAME = c_char_p(bytes(self.server_name.encode())) 910 | h = windll.kernel32.OpenFileMappingA(FILE_MAP_ALL_ACCESS, inherit_handle, HANDLE_NAME) 911 | is_opened = h != 0 912 | 913 | if is_opened: 914 | # close again to prevent handle leaks 915 | windll.kernel32.CloseHandle(h) 916 | 917 | return is_opened 918 | 919 | def _ensure_connected(self): 920 | if self.mfile is not None: 921 | return True 922 | 923 | try: 924 | if not self._is_mapped_file_present(): 925 | print(f"No TMI instance with server name {self.server_name} found, waiting for TMI instance to open..") 926 | while not self._is_mapped_file_present(): 927 | time.sleep(1) 928 | 929 | self.mfile = mmap.mmap(-1, self.buffer_size, tagname=self.server_name) 930 | return True 931 | except Exception as e: 932 | self.client.on_client_exception(self, e) 933 | 934 | return False 935 | 936 | def _wait_for_server_response(self, clear: bool = True): 937 | if self.mfile is None: 938 | return 939 | 940 | self.mfile.seek(0) 941 | while self._read_int32() != MessageType.S_RESPONSE | 0xFF00: 942 | self.mfile.seek(0) 943 | time.sleep(0) 944 | 945 | if clear: 946 | self._clear_buffer() 947 | 948 | def _respond_to_call(self, msgtype: int): 949 | msg = Message(MessageType.C_PROCESSED_CALL) 950 | msg.write_int32(msgtype) 951 | self._send_message(msg) 952 | 953 | def _send_message(self, message: Message): 954 | if self.mfile is None: 955 | return 956 | 957 | data = message.to_data() 958 | self.mfile.seek(0) 959 | self.mfile.write(data) 960 | 961 | self.mfile.seek(1) 962 | self.mfile.write(bytearray([0xFF])) 963 | 964 | def _clear_buffer(self): 965 | self.mfile.seek(0) 966 | self.mfile.write(self.empty_buffer) 967 | 968 | def _read(self, num_bytes: int, typestr: str): 969 | arr = self.mfile.read(num_bytes) 970 | try: 971 | return struct.unpack(typestr, arr)[0] 972 | except Exception as e: 973 | self.client.on_client_exception(self, e) 974 | return 0 975 | 976 | def _read_int(self, size): 977 | if size == 1: 978 | return self._read_uint8() 979 | if size == 2: 980 | return self._read_uint16() 981 | elif size == 4: 982 | return self._read_int32() 983 | 984 | return 0 985 | 986 | def _read_uint8(self): 987 | return self._read(1, 'B') 988 | 989 | def _read_int32(self): 990 | return self._read(4, 'i') 991 | 992 | def _read_uint32(self): 993 | return self._read(4, 'I') 994 | 995 | def _read_uint16(self): 996 | return self._read(2, 'H') 997 | 998 | def _read_string(self): 999 | chars = [chr(b) for b in self.__read_vector(1)] 1000 | return ''.join(chars) 1001 | 1002 | def _skip(self, n): 1003 | self.mfile.seek(self.mfile.tell() + n) 1004 | -------------------------------------------------------------------------------- /tminterface/structs.py: -------------------------------------------------------------------------------- 1 | from bytefield import ( 2 | ByteStruct, 3 | FloatField, 4 | IntegerField, 5 | ArrayField, 6 | BooleanField, 7 | ByteArrayField, 8 | StructField, 9 | StringField 10 | ) 11 | from enum import IntEnum 12 | from tminterface.constants import SIM_HAS_TIMERS, SIM_HAS_DYNA, SIM_HAS_PLAYER_INFO 13 | from tminterface.eventbuffer import Event 14 | import tminterface.util as util 15 | import numpy as np 16 | 17 | 18 | class PlayerInfoStruct(ByteStruct): 19 | """ 20 | Parameters: 21 | team: int 22 | prev_race_time: int 23 | race_time: int 24 | race_time : int 25 | race_best_time : int 26 | lap_start_time : int 27 | lap_time : int 28 | lap_best_time : int 29 | min_respawns : int 30 | nb_completed : int 31 | max_completed : int 32 | stunts_score : int 33 | best_stunts_score : int 34 | cur_checkpoint : int 35 | average_rank : float 36 | current_race_rank : int 37 | current_round_rank : int 38 | current_time : int 39 | race_state : int 40 | ready_enum : int 41 | round_num : int 42 | offset_current_cp : float 43 | cur_lap_cp_count : int 44 | cur_cp_count : int 45 | cur_lap : int 46 | race_finished : bool 47 | display_speed : int 48 | finish_not_passed : bool 49 | countdown_time : int 50 | rest : bytearray 51 | """ 52 | 53 | team = IntegerField(offset=576, signed=False) 54 | prev_race_time = IntegerField(offset=680) 55 | race_start_time = IntegerField(signed=False) 56 | race_time = IntegerField(signed=True) 57 | race_best_time = IntegerField(signed=False) 58 | lap_start_time = IntegerField(signed=False) 59 | lap_time = IntegerField(signed=False) 60 | lap_best_time = IntegerField() 61 | min_respawns = IntegerField(signed=False) 62 | nb_completed = IntegerField(signed=False) 63 | max_completed = IntegerField(signed=False) 64 | stunts_score = IntegerField(signed=False) 65 | best_stunts_score = IntegerField(signed=False) 66 | cur_checkpoint = IntegerField(signed=False) 67 | average_rank = FloatField() 68 | current_race_rank = IntegerField(signed=False) 69 | current_round_rank = IntegerField(signed=False) 70 | current_time = IntegerField(offset=776, signed=False) 71 | race_state = IntegerField(offset=788, signed=False) 72 | ready_enum = IntegerField(signed=False) 73 | round_num = IntegerField(signed=False) 74 | offset_current_cp = FloatField() 75 | cur_lap_cp_count = IntegerField(offset=816, signed=False) 76 | cur_cp_count = IntegerField(signed=False) 77 | cur_lap = IntegerField(signed=False) 78 | race_finished = BooleanField() 79 | display_speed = IntegerField() 80 | finish_not_passed = BooleanField() 81 | countdown_time = IntegerField(offset=916) 82 | rest = ByteArrayField(32) 83 | 84 | 85 | class HmsDynaStateStruct(ByteStruct): 86 | """ 87 | Parameters: 88 | quat: np.ndarray 89 | rotation: np.ndarray 90 | position: np.ndarray 91 | linear_speed: np.ndarray 92 | add_linear_speed: np.ndarray 93 | angular_speed: np.ndarray 94 | force: np.ndarray 95 | torque: np.ndarray 96 | inverse_inertia_tensor: np.ndarray 97 | unknown: float 98 | not_tweaked_linear_speed: np.ndarray 99 | owner: int 100 | """ 101 | 102 | quat = ArrayField(offset=0, shape=(4,), elem_field_type=FloatField) 103 | rotation = ArrayField(shape=(3, 3), elem_field_type=FloatField) 104 | position = ArrayField(shape=(3,), elem_field_type=FloatField) 105 | linear_speed = ArrayField(shape=(3,), elem_field_type=FloatField) 106 | add_linear_speed = ArrayField(shape=(3,), elem_field_type=FloatField) 107 | angular_speed = ArrayField(shape=(3,), elem_field_type=FloatField) 108 | force = ArrayField(shape=(3,), elem_field_type=FloatField) 109 | torque = ArrayField(shape=(3,), elem_field_type=FloatField) 110 | inverse_inertia_tensor = ArrayField(shape=(3, 3), elem_field_type=FloatField) 111 | unknown = FloatField() 112 | not_tweaked_linear_speed = ArrayField(shape=(3,), elem_field_type=FloatField) 113 | owner = IntegerField() 114 | 115 | @property 116 | def inverse_intertia_tensor(self): 117 | return self.inverse_inertia_tensor 118 | 119 | @inverse_intertia_tensor.setter 120 | def inverse_intertia_tensor(self, tensor): 121 | self.inverse_inertia_tensor = tensor 122 | 123 | 124 | class HmsDynaStruct(ByteStruct): 125 | """ 126 | Parameters: 127 | previous_state: HmsDynaStateStruct 128 | current_state: HmsDynaStateStruct 129 | temp_state: HmsDynaStateStruct 130 | rest: bytearray 131 | """ 132 | 133 | previous_state = StructField(offset=268, struct_type=HmsDynaStateStruct, instance_with_parent=False) 134 | current_state = StructField(struct_type=HmsDynaStateStruct, instance_with_parent=False) 135 | temp_state = StructField(struct_type=HmsDynaStateStruct, instance_with_parent=False) 136 | rest = ByteArrayField(616) 137 | 138 | @property 139 | def prev_state(self): 140 | return self.temp_state 141 | 142 | @prev_state.setter 143 | def prev_state(self, state): 144 | self.temp_state = state 145 | 146 | 147 | class SurfaceHandler(ByteStruct): 148 | """ 149 | Parameters: 150 | unknown: np.ndarray 151 | rotation: np.ndarray 152 | position: np.ndarray 153 | """ 154 | 155 | unknown = ArrayField(offset=4, shape=(4, 3), elem_field_type=FloatField) 156 | rotation = ArrayField(shape=(3, 3), elem_field_type=FloatField) 157 | position = ArrayField(shape=3, elem_field_type=FloatField) 158 | 159 | 160 | class RealTimeState(ByteStruct): 161 | """ 162 | Parameters: 163 | damper_absorb: float 164 | field_4: float 165 | field_8: float 166 | field_12: np.ndarray 167 | field_48: np.ndarray 168 | field_84: np.ndarray 169 | field_108: float 170 | has_ground_contact: bool 171 | contact_material_id: int 172 | is_sliding: bool 173 | relative_rotz_axis: np.ndarray 174 | nb_ground_contacts: int 175 | field_144: np.ndarray 176 | rest: bytearray 177 | """ 178 | 179 | damper_absorb = FloatField(offset=0) 180 | field_4 = FloatField() 181 | field_8 = FloatField() 182 | field_12 = ArrayField(shape=(3, 3), elem_field_type=FloatField) 183 | field_48 = ArrayField(shape=(3, 3), elem_field_type=FloatField) 184 | field_84 = ArrayField(shape=3, elem_field_type=FloatField) 185 | field_108 = FloatField(offset=108) 186 | has_ground_contact = BooleanField() 187 | contact_material_id = IntegerField() 188 | is_sliding = BooleanField() 189 | relative_rotz_axis = ArrayField(shape=3, elem_field_type=FloatField) 190 | nb_ground_contacts = IntegerField(offset=140) 191 | field_144 = ArrayField(shape=3, elem_field_type=FloatField) 192 | rest = ByteArrayField(12) 193 | 194 | 195 | class WheelState(ByteStruct): 196 | """ 197 | Parameters: 198 | rest: bytearray 199 | """ 200 | 201 | rest = ByteArrayField(100, offset=0) 202 | 203 | 204 | class SimulationWheel(ByteStruct): 205 | """ 206 | Parameters: 207 | steerable: bool 208 | field_8: int 209 | surface_handler: SurfaceHandler 210 | field_112: np.ndarray 211 | field_160: int 212 | field_164: int 213 | offset_from_vehicle: np.ndarray 214 | real_time_state: RealTimeState 215 | field_348: int 216 | contact_relative_local_distance: np.ndarray 217 | prev_sync_wheel_state: WheelState 218 | sync_wheel_state: WheelState 219 | field_564: WheelState 220 | async_wheel_state: WheelState 221 | """ 222 | 223 | steerable = BooleanField(offset=4) 224 | field_8 = IntegerField() 225 | surface_handler = StructField(SurfaceHandler) 226 | field_112 = ArrayField(shape=(4, 3), elem_field_type=FloatField) 227 | field_160 = IntegerField() 228 | field_164 = IntegerField() 229 | offset_from_vehicle = ArrayField(shape=3, elem_field_type=FloatField) 230 | real_time_state = StructField(RealTimeState) 231 | field_348 = IntegerField() 232 | contact_relative_local_distance = ArrayField(shape=3, elem_field_type=FloatField) 233 | prev_sync_wheel_state = StructField(WheelState, instance_with_parent=False) 234 | sync_wheel_state = StructField(WheelState, instance_with_parent=False) 235 | field_564 = StructField(WheelState, instance_with_parent=False) 236 | async_wheel_state = StructField(WheelState, instance_with_parent=False) 237 | 238 | 239 | class CheckpointTime(ByteStruct): 240 | """ 241 | Parameters: 242 | time: int 243 | stunts_score: int 244 | """ 245 | 246 | time = IntegerField(offset=0) 247 | stunts_score = IntegerField() 248 | 249 | 250 | class CheckpointData(ByteStruct): 251 | """ 252 | The CheckpointData object represents checkpoint state within the game. 253 | 254 | The game keeps track of two arrays that contain checkpoint information. 255 | The first "state" array is an array of booleans (a boolean is 4 bytes long) 256 | and keeps track of which checkpoints were already passed. The length of the 257 | array represents the real count of the checkpoints on current the map (including finish). 258 | This does not mean that to finish the race the player has to pass through this exact count 259 | of checkpoints. A map with 3 laps and 5 checkpoints will still contain only 5 checkpoint states. 260 | 261 | The second "times" array is an array of structures with 2 fields: time and an unknown field. 262 | This array holds the "logical" number of checkpoints that have to be passed (including finish). 263 | This means the total number of checkpoint passes, including the existence of laps. 264 | 265 | Arguments: 266 | cp_states (list): the checkpoint states array 267 | cp_times (list): the checkpoint times array, each element is a two element tuple of (time, flags) 268 | """ 269 | reserved = IntegerField(offset=0) 270 | cp_states_length = IntegerField() 271 | cp_states = ArrayField(shape=None, elem_field_type=BooleanField) 272 | cp_times_length = IntegerField() 273 | cp_times = ArrayField(shape=None, elem_field_type=CheckpointTime) 274 | 275 | def __init__(self, *args, **kwargs): 276 | if len(args) == 2 and isinstance(args[0], list) and isinstance(args[1], list): 277 | super().__init__(**kwargs) 278 | self.cp_states = args[0] 279 | self.cp_states_length = len(args[0]) 280 | self.cp_times = args[1] 281 | self.cp_times_length = len(args[1]) 282 | else: 283 | super().__init__(*args, **kwargs) 284 | 285 | def read_from_file(self, file): 286 | self.data += file.read(self.cp_states_length * 4) 287 | self.resize(CheckpointData.cp_states_field, self.cp_states_length) 288 | 289 | self.data += file.read(self.cp_times_length * CheckpointTime.min_size) 290 | self.resize(CheckpointData.cp_times_field, self.cp_times_length) 291 | 292 | 293 | class CachedInput(ByteStruct): 294 | """ 295 | Parameters: 296 | time: int 297 | event: Event 298 | """ 299 | 300 | time = IntegerField(offset=0) 301 | event = StructField(Event) 302 | 303 | 304 | class SceneVehicleCarState(ByteStruct): 305 | """ 306 | Parameters: 307 | speed_forward: float 308 | speed_sideward: float 309 | input_steer: float 310 | input_gas: float 311 | input_brake: float 312 | is_turbo: bool 313 | rpm: float 314 | gearbox_state: int 315 | rest: bytearray 316 | """ 317 | 318 | speed_forward = FloatField(offset=0) 319 | speed_sideward = FloatField() 320 | input_steer = FloatField() 321 | input_gas = FloatField() 322 | input_brake = FloatField() 323 | is_turbo = BooleanField() 324 | rpm = FloatField(offset=128) 325 | gearbox_state = IntegerField(offset=136) 326 | rest = ByteArrayField(28) 327 | 328 | 329 | class Engine(ByteStruct): 330 | """ 331 | Parameters: 332 | max_rpm: float 333 | braking_factor: float 334 | clamped_rpm: float 335 | actual_rpm: float 336 | slide_factor: float 337 | rear_gear: int 338 | gear: int 339 | """ 340 | 341 | max_rpm = FloatField(offset=0) 342 | braking_factor = FloatField(offset=20) 343 | clamped_rpm = FloatField() 344 | actual_rpm = FloatField() 345 | slide_factor = FloatField() 346 | rear_gear = IntegerField(offset=40) 347 | gear = IntegerField() 348 | 349 | 350 | class SceneVehicleCar(ByteStruct): 351 | """ 352 | Parameters: 353 | is_update_async: bool 354 | input_gas: float 355 | input_brake: float 356 | input_steer: float 357 | is_light_trials_set: bool 358 | horn_limit: int 359 | quality: int 360 | max_linear_speed: float 361 | gearbox_state: int 362 | block_flags: int 363 | prev_sync_vehicle_state: SceneVehicleCarState 364 | sync_vehicle_state: SceneVehicleCarState 365 | async_vehicle_state: SceneVehicleCarState 366 | prev_async_vehicle_state: SceneVehicleCarState 367 | engine: Engine 368 | has_any_lateral_contact: bool 369 | last_has_any_lateral_contact_time: int 370 | water_forces_applied: bool 371 | turning_rate: float 372 | turbo_boost_factor: float 373 | last_turbo_type_change_time: int 374 | last_turbo_time: int 375 | turbo_type: int 376 | roulette_value: int 377 | is_freewheeling: bool 378 | is_sliding: bool 379 | wheel_contact_absorb_counter: int 380 | burnout_state: int 381 | current_local_speed: np.ndarray 382 | total_central_force_added: np.ndarray 383 | is_rubber_ball: bool 384 | saved_state: np.ndarray 385 | """ 386 | 387 | is_update_async = BooleanField(offset=76) 388 | input_gas = FloatField() 389 | input_brake = FloatField() 390 | input_steer = FloatField() 391 | is_light_trials_set = BooleanField(offset=116) 392 | horn_limit = IntegerField(offset=148) 393 | quality = IntegerField(offset=164) 394 | max_linear_speed = FloatField(offset=736) 395 | gearbox_state = IntegerField() 396 | block_flags = IntegerField() 397 | prev_sync_vehicle_state = StructField(SceneVehicleCarState, instance_with_parent=False) 398 | sync_vehicle_state = StructField(SceneVehicleCarState, instance_with_parent=False) 399 | async_vehicle_state = StructField(SceneVehicleCarState, instance_with_parent=False) 400 | prev_async_vehicle_state = StructField(SceneVehicleCarState, instance_with_parent=False) 401 | engine = StructField(Engine, offset=1436) 402 | has_any_lateral_contact = BooleanField(offset=1500) 403 | last_has_any_lateral_contact_time = IntegerField() 404 | water_forces_applied = BooleanField() 405 | turning_rate = FloatField() 406 | turbo_boost_factor = FloatField(offset=1524) 407 | last_turbo_type_change_time = IntegerField() 408 | last_turbo_time = IntegerField() 409 | turbo_type = IntegerField() 410 | roulette_value = FloatField(offset=1544) 411 | is_freewheeling = BooleanField() 412 | is_sliding = BooleanField(offset=1576) 413 | wheel_contact_absorb_counter = IntegerField(offset=1660) 414 | burnout_state = IntegerField(offset=1692) 415 | current_local_speed = ArrayField(offset=1804, shape=3, elem_field_type=FloatField) 416 | total_central_force_added = ArrayField(offset=2072, shape=3, elem_field_type=FloatField) 417 | is_rubber_ball = BooleanField(offset=2116) 418 | saved_state = ArrayField(shape=(4, 3), elem_field_type=FloatField) 419 | 420 | 421 | class SimStateData(ByteStruct): 422 | """ 423 | The SimStateData object represents a full save state of the simulation state, 424 | including checkpoint and input information. 425 | 426 | The simulation state consists of raw memory buffers representing various 427 | information about the race state. This includes the entirety of the vehicle 428 | state as well as the player info and other state variables such as current 429 | checkpoint count and input state. 430 | 431 | The memory regions themselves are monitored by TMInterface itself and are used 432 | for functionality like save states or fast rewind in the bruteforce script. 433 | TMInterface may use additional native game methods to restore the state based 434 | on information present in some of these memory regions. It is important to note 435 | that the buffers contain instance specific fields such as pointers and array sizes. 436 | These are masked out automatically by TMInterface when restoring the state 437 | (and when calling TMInterface.rewind_to_state). 438 | 439 | To query input state of the simulation state regardless of context, 440 | use input_* (input_accelerate, input_brake etc.) accessors. 441 | 442 | Parameters: 443 | version: int 444 | context_mode: int 445 | flags: int 446 | timers: np.ndarray 447 | dyna: HmsDynaStruct 448 | scene_mobil: SceneVehicleCar 449 | simulation_wheels: np.ndarray 450 | plug_solid: bytes 451 | cmd_buffer_core: bytes 452 | player_info: PlayerInfoStruct 453 | internal_input_state: np.ndarray 454 | input_running_event: Event 455 | input_finish_event: Event 456 | input_accelerate_event: Event 457 | input_brake_event: Event 458 | input_left_event: Event 459 | input_right_event: Event 460 | input_steer_event: Event 461 | input_gas_event: Event 462 | """ 463 | version = IntegerField(offset=0, signed=False) 464 | context_mode = IntegerField(signed=False) 465 | flags = IntegerField(signed=False) 466 | timers = ArrayField(shape=53, elem_field_type=IntegerField) 467 | dyna = StructField(HmsDynaStruct, instance_with_parent=False) 468 | scene_mobil = StructField(SceneVehicleCar, instance_with_parent=False) 469 | simulation_wheels = ArrayField(shape=4, elem_field_type=SimulationWheel) 470 | plug_solid = ByteArrayField(68) 471 | cmd_buffer_core = ByteArrayField(264) 472 | player_info = StructField(PlayerInfoStruct, instance_with_parent=False) 473 | internal_input_state = ArrayField(shape=10, elem_field_type=CachedInput) 474 | 475 | input_running_event = StructField(Event, instance_with_parent=False) 476 | input_finish_event = StructField(Event, instance_with_parent=False) 477 | input_accelerate_event = StructField(Event, instance_with_parent=False) 478 | input_brake_event = StructField(Event, instance_with_parent=False) 479 | input_left_event = StructField(Event, instance_with_parent=False) 480 | input_right_event = StructField(Event, instance_with_parent=False) 481 | input_steer_event = StructField(Event, instance_with_parent=False) 482 | input_gas_event = StructField(Event, instance_with_parent=False) 483 | 484 | num_respawns = IntegerField(signed=False) 485 | 486 | cp_data = StructField(CheckpointData, instance_with_parent=False) 487 | 488 | @property 489 | def time(self) -> int: 490 | if (self.flags & SIM_HAS_TIMERS) == 0: 491 | return 0 492 | 493 | return self.timers[1] 494 | 495 | @property 496 | def position(self) -> list: 497 | if (self.flags & SIM_HAS_DYNA) == 0: 498 | return [0, 0, 0] 499 | 500 | return list(self.dyna.current_state.position) 501 | 502 | @property 503 | def velocity(self) -> list: 504 | if (self.flags & SIM_HAS_DYNA) == 0: 505 | return [0, 0, 0] 506 | 507 | return list(self.dyna.current_state.linear_speed) 508 | 509 | # Available only in run context 510 | @property 511 | def display_speed(self) -> int: 512 | if (self.flags & SIM_HAS_PLAYER_INFO) == 0: 513 | return 0 514 | 515 | return self.player_info.display_speed 516 | 517 | @position.setter 518 | def position(self, pos: list) -> bool: 519 | if (self.flags & SIM_HAS_DYNA) == 0: 520 | return False 521 | 522 | self.dyna.current_state.position = pos 523 | return True 524 | 525 | @velocity.setter 526 | def velocity(self, vel: list) -> bool: 527 | if (self.flags & SIM_HAS_DYNA) == 0: 528 | return False 529 | 530 | self.dyna.current_state.linear_speed = vel 531 | return True 532 | 533 | @property 534 | def rotation_matrix(self) -> list: 535 | if (self.flags & SIM_HAS_DYNA) == 0: 536 | return [[0, 0, 0]] * 3 537 | 538 | return self.dyna.current_state.rotation.to_numpy() 539 | 540 | @rotation_matrix.setter 541 | def rotation_matrix(self, matrix: list) -> bool: 542 | if (self.flags & SIM_HAS_DYNA) == 0: 543 | return False 544 | 545 | self.dyna.current_state.rotation = matrix 546 | self.dyna.current_state.quat = util.mat3_to_quat(matrix) 547 | 548 | @property 549 | def yaw_pitch_roll(self) -> np.array: 550 | if (self.flags & SIM_HAS_DYNA) == 0: 551 | return [0, 0, 0] 552 | 553 | mat = self.rotation_matrix 554 | return list(util.quat_to_ypw(util.mat3_to_quat(mat))) 555 | 556 | @property 557 | def race_time(self) -> int: 558 | if (self.flags & SIM_HAS_PLAYER_INFO) == 0: 559 | return False 560 | 561 | return self.player_info.race_time 562 | 563 | @property 564 | def rewind_time(self) -> int: 565 | return self.race_time + 10 566 | 567 | @property 568 | def input_accelerate(self) -> bool: 569 | return self.input_accelerate_event.binary_value 570 | 571 | @property 572 | def input_brake(self) -> bool: 573 | return self.input_brake_event.binary_value 574 | 575 | @property 576 | def input_left(self) -> bool: 577 | return self.input_left_event.binary_value 578 | 579 | @property 580 | def input_right(self) -> bool: 581 | return self.input_right_event.binary_value 582 | 583 | @property 584 | def input_steer(self) -> int: 585 | return self.input_steer_event.analog_value 586 | 587 | @property 588 | def input_gas(self) -> int: 589 | return self.input_gas_event.analog_value 590 | 591 | 592 | class BFTarget(IntEnum): 593 | """ 594 | The bruteforce metric that is being currently optimized. 595 | """ 596 | FINISH_TIME = 0 597 | CHECKPOINT_TIME = 1 598 | TRIGGER = 2 599 | DISTANCE_SPEED = 3 600 | 601 | 602 | class BFPhase(IntEnum): 603 | """ 604 | The phase in which the bruteforce script is currently working. 605 | 606 | The initial phase is executed at the beginning of the process and after each improvement. 607 | It is used primarily for collecting data about the race e.g: the race time, position of the car, 608 | checkpoint times etc. No state modification happens at this state and it is recommended to use this phase 609 | to collect information about the current solution. 610 | 611 | The search phase is when TMInterface is searching for a new improvement. In this phase, the process 612 | changes inputs according to the user settings and evaluates the solution based on the current target. 613 | """ 614 | INITIAL = 0 615 | SEARCH = 1 616 | 617 | 618 | class BFEvaluationDecision(IntEnum): 619 | """ 620 | The decision taken by the client in every bruteforce physics step. 621 | Returned in :meth:`Client.on_bruteforce_evaluate` in an BFEvaluationResponse instance. 622 | 623 | `CONTINUE`: run the default evaluation of the bruteforce script 624 | 625 | `DO_NOTHING`: do not run any evaluation that could result in accepting or rejecting the evaluated solution 626 | 627 | `ACCEPT`: accept the current solution as the new best one. Starts a new intial phase in the next physics step. 628 | 629 | `REJECT`: rejects the current solution and generates a new one for next evaluation 630 | 631 | `STOP`: stops the bruteforce script and lets the game simulate the race until the end 632 | """ 633 | CONTINUE = 0 634 | DO_NOTHING = 1 635 | ACCEPT = 2 636 | REJECT = 3 637 | STOP = 4 638 | 639 | 640 | class BFEvaluationInfo(ByteStruct): 641 | """ 642 | The bruteforce settings applied in the bruteforce process, including the current simulation race time. 643 | """ 644 | phase = IntegerField(signed=False) 645 | target = IntegerField(signed=False) 646 | time = IntegerField() 647 | modified_inputs_num = IntegerField() 648 | inputs_min_time = IntegerField() 649 | inputs_max_time = IntegerField() 650 | max_steer_diff = IntegerField() 651 | max_time_diff = IntegerField() 652 | override_stop_time = IntegerField() 653 | search_forever = BooleanField() 654 | inputs_extend_steer = BooleanField() 655 | 656 | def __init__(self, *args, **kwargs) -> None: 657 | super().__init__(*args, **kwargs) 658 | if not args: 659 | self.phase = BFPhase.INITIAL 660 | self.target = BFTarget.FINISH_TIME 661 | self.time = 0 662 | self.modified_inputs_num = -1 663 | self.inputs_min_time = -1 664 | self.inputs_max_time = -1 665 | self.max_steer_diff = -1 666 | self.max_time_diff = -1 667 | self.override_stop_time = -1 668 | self.search_forever = False 669 | self.inputs_extend_steer = False 670 | 671 | 672 | class BFEvaluationResponse(ByteStruct): 673 | """ 674 | The response object sent by :meth:`Client.on_bruteforce_evaluate`. 675 | 676 | If `decision` is set to :class:`BFEvaluationDecision.REJECT`, 677 | you are allowed to change the inputs manually via the :meth:`TMInterface.set_event_buffer` method and 678 | set the `rewind_time` to `timestamp - 10` where `timestamp` is the first input that has been 679 | changed by your algorithm. Otherwise, TMInterface will automatically randomize the inputs according 680 | to the current settings itself. 681 | """ 682 | decision = IntegerField(signed=False) 683 | rewind_time = IntegerField() 684 | 685 | def __init__(self) -> None: 686 | super().__init__() 687 | self.decision = BFEvaluationDecision.CONTINUE 688 | self.rewind_time = -1 689 | 690 | 691 | class ClassicString(ByteStruct): 692 | """ 693 | A string sent by the client to TMInterface. 694 | """ 695 | command_length = IntegerField() 696 | command = StringField(None) 697 | 698 | def __init__(self, command: str) -> None: 699 | super().__init__() 700 | self.command_length = len(command) 701 | self.command = command 702 | -------------------------------------------------------------------------------- /tminterface/util.py: -------------------------------------------------------------------------------- 1 | from numpy import int32 2 | import numpy as np 3 | import math 4 | 5 | EPSILON = 0.00001 6 | 7 | 8 | def data_to_analog_value(data: int) -> int: 9 | """ 10 | Converts an internal analog state value to a [-65536, 65536] range. 11 | 12 | The function supports values outside the normal range, that is 13 | you can convert values in the extended range as well. 14 | 15 | Args: 16 | data (int): the internal value, usually stored in an event buffer 17 | 18 | Returns: 19 | int: the converted value 20 | """ 21 | val = int32(data) 22 | val <<= int32(8) 23 | val >>= int32(8) 24 | return -val 25 | 26 | 27 | def analog_value_to_data(value: int) -> int: 28 | """ 29 | Converts a value in [-65536, 65536] range to an internal analog state value. 30 | 31 | The function supports values outside the normal range, that is 32 | you can convert values in the extended range as well. 33 | 34 | Args: 35 | data (int): the value to convert 36 | 37 | Returns: 38 | int: the converted value 39 | """ 40 | value = -value 41 | value <<= int32(8) 42 | value >>= int32(8) 43 | return value 44 | 45 | 46 | def quat_to_ypw(quat: np.array) -> np.array: 47 | """ 48 | Converts a quaternion to yaw, pitch and roll values. 49 | 50 | This function uses the internal implementation that the game 51 | uses to convert quaternions to yaw, pitch and roll. 52 | 53 | The function itself is constructed from reverse engineering 54 | the code of the game. Note that this particular implementation 55 | is not 100% compatible with the actual assembly code, however it 56 | should produce the same results in most cases. 57 | 58 | Args: 59 | quat (np.array): the quaternion to convert (x, y, z, w) 60 | 61 | Returns: 62 | np.array: an array containing 3 elements: yaw, pitch and roll 63 | """ 64 | t0 = quat[2] * quat[1] + quat[3] * quat[0] 65 | 66 | if abs(t0 + 0.5) < EPSILON or t0 + 0.5 <= 0: 67 | yaw = math.atan2(quat[1], quat[0]) 68 | return np.array([yaw * 2, -1.57079637, 0]) 69 | 70 | if abs(t0 - 0.5) < EPSILON or t0 - 0.5 >= 0: 71 | yaw = math.atan2(quat[1], quat[0]) 72 | return np.array([-yaw * 2, 1.57079637, 0]) 73 | 74 | yaw = math.atan2(2.0 * (quat[2] * quat[0] - quat[3] * quat[1]), 1.0 - (quat[3] * quat[3] + quat[2] * quat[2]) * 2.0) 75 | roll = math.asin(2.0 * t0) 76 | pitch = math.atan2(2.0 * (quat[0] * quat[1] - quat[2] * quat[3]), 1.0 - 2.0 * (quat[1] * quat[1] + quat[3] * quat[3])) 77 | return np.array([yaw, pitch, roll]) 78 | 79 | 80 | def mat3_to_quat(mat: np.array) -> np.array: 81 | """ 82 | Converts a rotation matrix to a quaternion. 83 | 84 | This function uses the internal implementation that the game 85 | uses to convert a rotation matrix to a quaternion. 86 | 87 | The function itself is constructed from reverse engineering 88 | the code of the game. This particular implementation should be 89 | 100% compatible with the original method the game uses. 90 | Note however that this compatibility is not guaranteed. 91 | 92 | Args: 93 | mat (np.array): a 3x3 rotation matrix to convert 94 | 95 | Returns: 96 | np.array: the quaternion consisting of 4 elements (x, y, z, w) 97 | """ 98 | trace = np.trace(mat) 99 | 100 | if trace > 0: 101 | trace += 1 102 | trace_squared = math.sqrt(trace) 103 | 104 | trace = 0.5 / trace_squared 105 | return np.array([ 106 | trace_squared / 2, 107 | trace * (mat[2, 1] - mat[1, 2]), 108 | trace * (mat[0, 2] - mat[2, 0]), 109 | trace * (mat[1, 0] - mat[0, 1]) 110 | ]) 111 | 112 | index = 0 113 | if mat[1, 1] > mat[0, 0]: 114 | index = 1 115 | elif mat[2, 2] > mat[0, 0]: 116 | index = 2 117 | 118 | indexes = [1, 2, 0] 119 | var_1 = indexes[index] 120 | var_2 = indexes[var_1] 121 | 122 | trace = mat[index, index] - (mat[var_2, var_2] + mat[var_1, var_1]) + 1.0 123 | 124 | trace_squared = math.sqrt(trace) 125 | trace = 0.5 / trace_squared 126 | 127 | quat = np.zeros(4) 128 | quat[0] = trace * (mat[var_2, var_1] - mat[var_1, var_2]) 129 | quat[index + 1] = trace_squared / 2 130 | quat[var_1 + 1] = trace * (mat[index, var_1] + mat[var_1, index]) 131 | quat[var_2 + 1] = trace * (mat[index, var_2] + mat[var_2, index]) 132 | return quat 133 | --------------------------------------------------------------------------------