├── .gitignore ├── COPYING ├── Cargo.lock ├── Cargo.toml ├── Changes.md ├── LICENSE ├── README.md ├── build.rs └── src ├── config.rs ├── events.rs ├── game.rs ├── game ├── controls.rs ├── file.rs ├── file │ ├── control_bindings.rs │ └── journal.rs └── ship.rs ├── lib.rs ├── main.rs ├── resources ├── edxlc.ico └── edxlc.rc ├── x52pro.rs └── x52pro ├── device.rs ├── direct_output.rs ├── light_mode_to_state_mapper.rs └── status_level_to_mode_mapper.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | 3 | # Written out automatically when executing `cargo run`. 4 | edxlc.toml -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 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 | . -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | [[package]] 4 | name = "aho-corasick" 5 | version = "0.7.15" 6 | source = "registry+https://github.com/rust-lang/crates.io-index" 7 | checksum = "7404febffaa47dac81aa44dba71523c9d069b1bdc50a77db41195149e17f68e5" 8 | dependencies = [ 9 | "memchr", 10 | ] 11 | 12 | [[package]] 13 | name = "arrayref" 14 | version = "0.3.6" 15 | source = "registry+https://github.com/rust-lang/crates.io-index" 16 | checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" 17 | 18 | [[package]] 19 | name = "arrayvec" 20 | version = "0.5.2" 21 | source = "registry+https://github.com/rust-lang/crates.io-index" 22 | checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" 23 | 24 | [[package]] 25 | name = "atty" 26 | version = "0.2.14" 27 | source = "registry+https://github.com/rust-lang/crates.io-index" 28 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 29 | dependencies = [ 30 | "hermit-abi", 31 | "libc", 32 | "winapi 0.3.9", 33 | ] 34 | 35 | [[package]] 36 | name = "autocfg" 37 | version = "1.0.1" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" 40 | 41 | [[package]] 42 | name = "base64" 43 | version = "0.13.0" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" 46 | 47 | [[package]] 48 | name = "bitflags" 49 | version = "1.2.1" 50 | source = "registry+https://github.com/rust-lang/crates.io-index" 51 | checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" 52 | 53 | [[package]] 54 | name = "blake2b_simd" 55 | version = "0.5.11" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "afa748e348ad3be8263be728124b24a24f268266f6f5d58af9d75f6a40b5c587" 58 | dependencies = [ 59 | "arrayref", 60 | "arrayvec", 61 | "constant_time_eq", 62 | ] 63 | 64 | [[package]] 65 | name = "cc" 66 | version = "1.0.67" 67 | source = "registry+https://github.com/rust-lang/crates.io-index" 68 | checksum = "e3c69b077ad434294d3ce9f1f6143a2a4b89a8a2d54ef813d85003a4fd1137fd" 69 | 70 | [[package]] 71 | name = "cfg-if" 72 | version = "0.1.10" 73 | source = "registry+https://github.com/rust-lang/crates.io-index" 74 | checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" 75 | 76 | [[package]] 77 | name = "cfg-if" 78 | version = "1.0.0" 79 | source = "registry+https://github.com/rust-lang/crates.io-index" 80 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 81 | 82 | [[package]] 83 | name = "constant_time_eq" 84 | version = "0.1.5" 85 | source = "registry+https://github.com/rust-lang/crates.io-index" 86 | checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" 87 | 88 | [[package]] 89 | name = "crossbeam-utils" 90 | version = "0.8.3" 91 | source = "registry+https://github.com/rust-lang/crates.io-index" 92 | checksum = "e7e9d99fa91428effe99c5c6d4634cdeba32b8cf784fc428a2a687f61a952c49" 93 | dependencies = [ 94 | "autocfg", 95 | "cfg-if 1.0.0", 96 | "lazy_static", 97 | ] 98 | 99 | [[package]] 100 | name = "ctrlc" 101 | version = "3.1.8" 102 | source = "registry+https://github.com/rust-lang/crates.io-index" 103 | checksum = "c15b8ec3b5755a188c141c1f6a98e76de31b936209bf066b647979e2a84764a9" 104 | dependencies = [ 105 | "nix", 106 | "winapi 0.3.9", 107 | ] 108 | 109 | [[package]] 110 | name = "dirs" 111 | version = "3.0.1" 112 | source = "registry+https://github.com/rust-lang/crates.io-index" 113 | checksum = "142995ed02755914747cc6ca76fc7e4583cd18578746716d0508ea6ed558b9ff" 114 | dependencies = [ 115 | "dirs-sys", 116 | ] 117 | 118 | [[package]] 119 | name = "dirs-sys" 120 | version = "0.3.5" 121 | source = "registry+https://github.com/rust-lang/crates.io-index" 122 | checksum = "8e93d7f5705de3e49895a2b5e0b8855a1c27f080192ae9c32a6432d50741a57a" 123 | dependencies = [ 124 | "libc", 125 | "redox_users", 126 | "winapi 0.3.9", 127 | ] 128 | 129 | [[package]] 130 | name = "edxlc" 131 | version = "1.13.0" 132 | dependencies = [ 133 | "ctrlc", 134 | "dirs", 135 | "embed-resource", 136 | "enum-iterator", 137 | "env_logger", 138 | "glob", 139 | "hotwatch", 140 | "libc", 141 | "libloading", 142 | "log", 143 | "serde", 144 | "serde-xml-rs", 145 | "serde_json", 146 | "toml", 147 | "winapi 0.3.9", 148 | "winreg 0.10.1", 149 | ] 150 | 151 | [[package]] 152 | name = "embed-resource" 153 | version = "1.6.2" 154 | source = "registry+https://github.com/rust-lang/crates.io-index" 155 | checksum = "d0ea6debf1262982d24274dc85f3374b42534df140897c25cea86b81e017d470" 156 | dependencies = [ 157 | "cc", 158 | "vswhom", 159 | "winreg 0.8.0", 160 | ] 161 | 162 | [[package]] 163 | name = "enum-iterator" 164 | version = "0.7.0" 165 | source = "registry+https://github.com/rust-lang/crates.io-index" 166 | checksum = "4eeac5c5edb79e4e39fe8439ef35207780a11f69c52cbe424ce3dfad4cb78de6" 167 | dependencies = [ 168 | "enum-iterator-derive", 169 | ] 170 | 171 | [[package]] 172 | name = "enum-iterator-derive" 173 | version = "0.7.0" 174 | source = "registry+https://github.com/rust-lang/crates.io-index" 175 | checksum = "c134c37760b27a871ba422106eedbb8247da973a09e82558bf26d619c882b159" 176 | dependencies = [ 177 | "proc-macro2", 178 | "quote", 179 | "syn", 180 | ] 181 | 182 | [[package]] 183 | name = "env_logger" 184 | version = "0.8.3" 185 | source = "registry+https://github.com/rust-lang/crates.io-index" 186 | checksum = "17392a012ea30ef05a610aa97dfb49496e71c9f676b27879922ea5bdf60d9d3f" 187 | dependencies = [ 188 | "atty", 189 | "humantime", 190 | "log", 191 | "regex", 192 | "termcolor", 193 | ] 194 | 195 | [[package]] 196 | name = "filetime" 197 | version = "0.2.14" 198 | source = "registry+https://github.com/rust-lang/crates.io-index" 199 | checksum = "1d34cfa13a63ae058bfa601fe9e313bbdb3746427c1459185464ce0fcf62e1e8" 200 | dependencies = [ 201 | "cfg-if 1.0.0", 202 | "libc", 203 | "redox_syscall 0.2.5", 204 | "winapi 0.3.9", 205 | ] 206 | 207 | [[package]] 208 | name = "fsevent" 209 | version = "0.4.0" 210 | source = "registry+https://github.com/rust-lang/crates.io-index" 211 | checksum = "5ab7d1bd1bd33cc98b0889831b72da23c0aa4df9cec7e0702f46ecea04b35db6" 212 | dependencies = [ 213 | "bitflags", 214 | "fsevent-sys", 215 | ] 216 | 217 | [[package]] 218 | name = "fsevent-sys" 219 | version = "2.0.1" 220 | source = "registry+https://github.com/rust-lang/crates.io-index" 221 | checksum = "f41b048a94555da0f42f1d632e2e19510084fb8e303b0daa2816e733fb3644a0" 222 | dependencies = [ 223 | "libc", 224 | ] 225 | 226 | [[package]] 227 | name = "fuchsia-zircon" 228 | version = "0.3.3" 229 | source = "registry+https://github.com/rust-lang/crates.io-index" 230 | checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" 231 | dependencies = [ 232 | "bitflags", 233 | "fuchsia-zircon-sys", 234 | ] 235 | 236 | [[package]] 237 | name = "fuchsia-zircon-sys" 238 | version = "0.3.3" 239 | source = "registry+https://github.com/rust-lang/crates.io-index" 240 | checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" 241 | 242 | [[package]] 243 | name = "getrandom" 244 | version = "0.1.16" 245 | source = "registry+https://github.com/rust-lang/crates.io-index" 246 | checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" 247 | dependencies = [ 248 | "cfg-if 1.0.0", 249 | "libc", 250 | "wasi", 251 | ] 252 | 253 | [[package]] 254 | name = "glob" 255 | version = "0.3.0" 256 | source = "registry+https://github.com/rust-lang/crates.io-index" 257 | checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" 258 | 259 | [[package]] 260 | name = "hermit-abi" 261 | version = "0.1.18" 262 | source = "registry+https://github.com/rust-lang/crates.io-index" 263 | checksum = "322f4de77956e22ed0e5032c359a0f1273f1f7f0d79bfa3b8ffbc730d7fbcc5c" 264 | dependencies = [ 265 | "libc", 266 | ] 267 | 268 | [[package]] 269 | name = "hotwatch" 270 | version = "0.4.5" 271 | source = "registry+https://github.com/rust-lang/crates.io-index" 272 | checksum = "d61ee702e77f237b41761361a82e5c4bf6277dbb4bc8b6b7d745cb249cc82b31" 273 | dependencies = [ 274 | "log", 275 | "notify", 276 | ] 277 | 278 | [[package]] 279 | name = "humantime" 280 | version = "2.1.0" 281 | source = "registry+https://github.com/rust-lang/crates.io-index" 282 | checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" 283 | 284 | [[package]] 285 | name = "inotify" 286 | version = "0.7.1" 287 | source = "registry+https://github.com/rust-lang/crates.io-index" 288 | checksum = "4816c66d2c8ae673df83366c18341538f234a26d65a9ecea5c348b453ac1d02f" 289 | dependencies = [ 290 | "bitflags", 291 | "inotify-sys", 292 | "libc", 293 | ] 294 | 295 | [[package]] 296 | name = "inotify-sys" 297 | version = "0.1.5" 298 | source = "registry+https://github.com/rust-lang/crates.io-index" 299 | checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" 300 | dependencies = [ 301 | "libc", 302 | ] 303 | 304 | [[package]] 305 | name = "iovec" 306 | version = "0.1.4" 307 | source = "registry+https://github.com/rust-lang/crates.io-index" 308 | checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" 309 | dependencies = [ 310 | "libc", 311 | ] 312 | 313 | [[package]] 314 | name = "itoa" 315 | version = "0.4.7" 316 | source = "registry+https://github.com/rust-lang/crates.io-index" 317 | checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" 318 | 319 | [[package]] 320 | name = "kernel32-sys" 321 | version = "0.2.2" 322 | source = "registry+https://github.com/rust-lang/crates.io-index" 323 | checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" 324 | dependencies = [ 325 | "winapi 0.2.8", 326 | "winapi-build", 327 | ] 328 | 329 | [[package]] 330 | name = "lazy_static" 331 | version = "1.4.0" 332 | source = "registry+https://github.com/rust-lang/crates.io-index" 333 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 334 | 335 | [[package]] 336 | name = "lazycell" 337 | version = "1.3.0" 338 | source = "registry+https://github.com/rust-lang/crates.io-index" 339 | checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" 340 | 341 | [[package]] 342 | name = "libc" 343 | version = "0.2.91" 344 | source = "registry+https://github.com/rust-lang/crates.io-index" 345 | checksum = "8916b1f6ca17130ec6568feccee27c156ad12037880833a3b842a823236502e7" 346 | 347 | [[package]] 348 | name = "libloading" 349 | version = "0.7.0" 350 | source = "registry+https://github.com/rust-lang/crates.io-index" 351 | checksum = "6f84d96438c15fcd6c3f244c8fce01d1e2b9c6b5623e9c711dc9286d8fc92d6a" 352 | dependencies = [ 353 | "cfg-if 1.0.0", 354 | "winapi 0.3.9", 355 | ] 356 | 357 | [[package]] 358 | name = "log" 359 | version = "0.4.14" 360 | source = "registry+https://github.com/rust-lang/crates.io-index" 361 | checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" 362 | dependencies = [ 363 | "cfg-if 1.0.0", 364 | ] 365 | 366 | [[package]] 367 | name = "memchr" 368 | version = "2.3.4" 369 | source = "registry+https://github.com/rust-lang/crates.io-index" 370 | checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525" 371 | 372 | [[package]] 373 | name = "mio" 374 | version = "0.6.23" 375 | source = "registry+https://github.com/rust-lang/crates.io-index" 376 | checksum = "4afd66f5b91bf2a3bc13fad0e21caedac168ca4c707504e75585648ae80e4cc4" 377 | dependencies = [ 378 | "cfg-if 0.1.10", 379 | "fuchsia-zircon", 380 | "fuchsia-zircon-sys", 381 | "iovec", 382 | "kernel32-sys", 383 | "libc", 384 | "log", 385 | "miow", 386 | "net2", 387 | "slab", 388 | "winapi 0.2.8", 389 | ] 390 | 391 | [[package]] 392 | name = "mio-extras" 393 | version = "2.0.6" 394 | source = "registry+https://github.com/rust-lang/crates.io-index" 395 | checksum = "52403fe290012ce777c4626790c8951324a2b9e3316b3143779c72b029742f19" 396 | dependencies = [ 397 | "lazycell", 398 | "log", 399 | "mio", 400 | "slab", 401 | ] 402 | 403 | [[package]] 404 | name = "miow" 405 | version = "0.2.2" 406 | source = "registry+https://github.com/rust-lang/crates.io-index" 407 | checksum = "ebd808424166322d4a38da87083bfddd3ac4c131334ed55856112eb06d46944d" 408 | dependencies = [ 409 | "kernel32-sys", 410 | "net2", 411 | "winapi 0.2.8", 412 | "ws2_32-sys", 413 | ] 414 | 415 | [[package]] 416 | name = "net2" 417 | version = "0.2.37" 418 | source = "registry+https://github.com/rust-lang/crates.io-index" 419 | checksum = "391630d12b68002ae1e25e8f974306474966550ad82dac6886fb8910c19568ae" 420 | dependencies = [ 421 | "cfg-if 0.1.10", 422 | "libc", 423 | "winapi 0.3.9", 424 | ] 425 | 426 | [[package]] 427 | name = "nix" 428 | version = "0.20.0" 429 | source = "registry+https://github.com/rust-lang/crates.io-index" 430 | checksum = "fa9b4819da1bc61c0ea48b63b7bc8604064dd43013e7cc325df098d49cd7c18a" 431 | dependencies = [ 432 | "bitflags", 433 | "cc", 434 | "cfg-if 1.0.0", 435 | "libc", 436 | ] 437 | 438 | [[package]] 439 | name = "notify" 440 | version = "4.0.15" 441 | source = "registry+https://github.com/rust-lang/crates.io-index" 442 | checksum = "80ae4a7688d1fab81c5bf19c64fc8db920be8d519ce6336ed4e7efe024724dbd" 443 | dependencies = [ 444 | "bitflags", 445 | "filetime", 446 | "fsevent", 447 | "fsevent-sys", 448 | "inotify", 449 | "libc", 450 | "mio", 451 | "mio-extras", 452 | "walkdir", 453 | "winapi 0.3.9", 454 | ] 455 | 456 | [[package]] 457 | name = "proc-macro2" 458 | version = "1.0.26" 459 | source = "registry+https://github.com/rust-lang/crates.io-index" 460 | checksum = "a152013215dca273577e18d2bf00fa862b89b24169fb78c4c95aeb07992c9cec" 461 | dependencies = [ 462 | "unicode-xid", 463 | ] 464 | 465 | [[package]] 466 | name = "quote" 467 | version = "1.0.9" 468 | source = "registry+https://github.com/rust-lang/crates.io-index" 469 | checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" 470 | dependencies = [ 471 | "proc-macro2", 472 | ] 473 | 474 | [[package]] 475 | name = "redox_syscall" 476 | version = "0.1.57" 477 | source = "registry+https://github.com/rust-lang/crates.io-index" 478 | checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" 479 | 480 | [[package]] 481 | name = "redox_syscall" 482 | version = "0.2.5" 483 | source = "registry+https://github.com/rust-lang/crates.io-index" 484 | checksum = "94341e4e44e24f6b591b59e47a8a027df12e008d73fd5672dbea9cc22f4507d9" 485 | dependencies = [ 486 | "bitflags", 487 | ] 488 | 489 | [[package]] 490 | name = "redox_users" 491 | version = "0.3.5" 492 | source = "registry+https://github.com/rust-lang/crates.io-index" 493 | checksum = "de0737333e7a9502c789a36d7c7fa6092a49895d4faa31ca5df163857ded2e9d" 494 | dependencies = [ 495 | "getrandom", 496 | "redox_syscall 0.1.57", 497 | "rust-argon2", 498 | ] 499 | 500 | [[package]] 501 | name = "regex" 502 | version = "1.4.6" 503 | source = "registry+https://github.com/rust-lang/crates.io-index" 504 | checksum = "2a26af418b574bd56588335b3a3659a65725d4e636eb1016c2f9e3b38c7cc759" 505 | dependencies = [ 506 | "aho-corasick", 507 | "memchr", 508 | "regex-syntax", 509 | ] 510 | 511 | [[package]] 512 | name = "regex-syntax" 513 | version = "0.6.23" 514 | source = "registry+https://github.com/rust-lang/crates.io-index" 515 | checksum = "24d5f089152e60f62d28b835fbff2cd2e8dc0baf1ac13343bef92ab7eed84548" 516 | 517 | [[package]] 518 | name = "rust-argon2" 519 | version = "0.8.3" 520 | source = "registry+https://github.com/rust-lang/crates.io-index" 521 | checksum = "4b18820d944b33caa75a71378964ac46f58517c92b6ae5f762636247c09e78fb" 522 | dependencies = [ 523 | "base64", 524 | "blake2b_simd", 525 | "constant_time_eq", 526 | "crossbeam-utils", 527 | ] 528 | 529 | [[package]] 530 | name = "ryu" 531 | version = "1.0.5" 532 | source = "registry+https://github.com/rust-lang/crates.io-index" 533 | checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" 534 | 535 | [[package]] 536 | name = "same-file" 537 | version = "1.0.6" 538 | source = "registry+https://github.com/rust-lang/crates.io-index" 539 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 540 | dependencies = [ 541 | "winapi-util", 542 | ] 543 | 544 | [[package]] 545 | name = "serde" 546 | version = "1.0.125" 547 | source = "registry+https://github.com/rust-lang/crates.io-index" 548 | checksum = "558dc50e1a5a5fa7112ca2ce4effcb321b0300c0d4ccf0776a9f60cd89031171" 549 | dependencies = [ 550 | "serde_derive", 551 | ] 552 | 553 | [[package]] 554 | name = "serde-xml-rs" 555 | version = "0.4.1" 556 | source = "registry+https://github.com/rust-lang/crates.io-index" 557 | checksum = "f0bf1ba0696ccf0872866277143ff1fd14d22eec235d2b23702f95e6660f7dfa" 558 | dependencies = [ 559 | "log", 560 | "serde", 561 | "thiserror", 562 | "xml-rs", 563 | ] 564 | 565 | [[package]] 566 | name = "serde_derive" 567 | version = "1.0.125" 568 | source = "registry+https://github.com/rust-lang/crates.io-index" 569 | checksum = "b093b7a2bb58203b5da3056c05b4ec1fed827dcfdb37347a8841695263b3d06d" 570 | dependencies = [ 571 | "proc-macro2", 572 | "quote", 573 | "syn", 574 | ] 575 | 576 | [[package]] 577 | name = "serde_json" 578 | version = "1.0.64" 579 | source = "registry+https://github.com/rust-lang/crates.io-index" 580 | checksum = "799e97dc9fdae36a5c8b8f2cae9ce2ee9fdce2058c57a93e6099d919fd982f79" 581 | dependencies = [ 582 | "itoa", 583 | "ryu", 584 | "serde", 585 | ] 586 | 587 | [[package]] 588 | name = "slab" 589 | version = "0.4.2" 590 | source = "registry+https://github.com/rust-lang/crates.io-index" 591 | checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" 592 | 593 | [[package]] 594 | name = "syn" 595 | version = "1.0.68" 596 | source = "registry+https://github.com/rust-lang/crates.io-index" 597 | checksum = "3ce15dd3ed8aa2f8eeac4716d6ef5ab58b6b9256db41d7e1a0224c2788e8fd87" 598 | dependencies = [ 599 | "proc-macro2", 600 | "quote", 601 | "unicode-xid", 602 | ] 603 | 604 | [[package]] 605 | name = "termcolor" 606 | version = "1.1.2" 607 | source = "registry+https://github.com/rust-lang/crates.io-index" 608 | checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" 609 | dependencies = [ 610 | "winapi-util", 611 | ] 612 | 613 | [[package]] 614 | name = "thiserror" 615 | version = "1.0.24" 616 | source = "registry+https://github.com/rust-lang/crates.io-index" 617 | checksum = "e0f4a65597094d4483ddaed134f409b2cb7c1beccf25201a9f73c719254fa98e" 618 | dependencies = [ 619 | "thiserror-impl", 620 | ] 621 | 622 | [[package]] 623 | name = "thiserror-impl" 624 | version = "1.0.24" 625 | source = "registry+https://github.com/rust-lang/crates.io-index" 626 | checksum = "7765189610d8241a44529806d6fd1f2e0a08734313a35d5b3a556f92b381f3c0" 627 | dependencies = [ 628 | "proc-macro2", 629 | "quote", 630 | "syn", 631 | ] 632 | 633 | [[package]] 634 | name = "toml" 635 | version = "0.5.8" 636 | source = "registry+https://github.com/rust-lang/crates.io-index" 637 | checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" 638 | dependencies = [ 639 | "serde", 640 | ] 641 | 642 | [[package]] 643 | name = "unicode-xid" 644 | version = "0.2.1" 645 | source = "registry+https://github.com/rust-lang/crates.io-index" 646 | checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" 647 | 648 | [[package]] 649 | name = "vswhom" 650 | version = "0.1.0" 651 | source = "registry+https://github.com/rust-lang/crates.io-index" 652 | checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" 653 | dependencies = [ 654 | "libc", 655 | "vswhom-sys", 656 | ] 657 | 658 | [[package]] 659 | name = "vswhom-sys" 660 | version = "0.1.0" 661 | source = "registry+https://github.com/rust-lang/crates.io-index" 662 | checksum = "fc2f5402d3d0e79a069714f7b48e3ecc60be7775a2c049cb839457457a239532" 663 | dependencies = [ 664 | "cc", 665 | "libc", 666 | ] 667 | 668 | [[package]] 669 | name = "walkdir" 670 | version = "2.3.2" 671 | source = "registry+https://github.com/rust-lang/crates.io-index" 672 | checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" 673 | dependencies = [ 674 | "same-file", 675 | "winapi 0.3.9", 676 | "winapi-util", 677 | ] 678 | 679 | [[package]] 680 | name = "wasi" 681 | version = "0.9.0+wasi-snapshot-preview1" 682 | source = "registry+https://github.com/rust-lang/crates.io-index" 683 | checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" 684 | 685 | [[package]] 686 | name = "winapi" 687 | version = "0.2.8" 688 | source = "registry+https://github.com/rust-lang/crates.io-index" 689 | checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" 690 | 691 | [[package]] 692 | name = "winapi" 693 | version = "0.3.9" 694 | source = "registry+https://github.com/rust-lang/crates.io-index" 695 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 696 | dependencies = [ 697 | "winapi-i686-pc-windows-gnu", 698 | "winapi-x86_64-pc-windows-gnu", 699 | ] 700 | 701 | [[package]] 702 | name = "winapi-build" 703 | version = "0.1.1" 704 | source = "registry+https://github.com/rust-lang/crates.io-index" 705 | checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" 706 | 707 | [[package]] 708 | name = "winapi-i686-pc-windows-gnu" 709 | version = "0.4.0" 710 | source = "registry+https://github.com/rust-lang/crates.io-index" 711 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 712 | 713 | [[package]] 714 | name = "winapi-util" 715 | version = "0.1.5" 716 | source = "registry+https://github.com/rust-lang/crates.io-index" 717 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 718 | dependencies = [ 719 | "winapi 0.3.9", 720 | ] 721 | 722 | [[package]] 723 | name = "winapi-x86_64-pc-windows-gnu" 724 | version = "0.4.0" 725 | source = "registry+https://github.com/rust-lang/crates.io-index" 726 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 727 | 728 | [[package]] 729 | name = "winreg" 730 | version = "0.8.0" 731 | source = "registry+https://github.com/rust-lang/crates.io-index" 732 | checksum = "d107f8c6e916235c4c01cabb3e8acf7bea8ef6a63ca2e7fa0527c049badfc48c" 733 | dependencies = [ 734 | "winapi 0.3.9", 735 | ] 736 | 737 | [[package]] 738 | name = "winreg" 739 | version = "0.10.1" 740 | source = "registry+https://github.com/rust-lang/crates.io-index" 741 | checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" 742 | dependencies = [ 743 | "winapi 0.3.9", 744 | ] 745 | 746 | [[package]] 747 | name = "ws2_32-sys" 748 | version = "0.2.1" 749 | source = "registry+https://github.com/rust-lang/crates.io-index" 750 | checksum = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e" 751 | dependencies = [ 752 | "winapi 0.2.8", 753 | "winapi-build", 754 | ] 755 | 756 | [[package]] 757 | name = "xml-rs" 758 | version = "0.8.3" 759 | source = "registry+https://github.com/rust-lang/crates.io-index" 760 | checksum = "b07db065a5cf61a7e4ba64f29e67db906fb1787316516c4e6e5ff0fea1efcd8a" 761 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "edxlc" 3 | version = "1.13.0" 4 | authors = ["Andrew Smith"] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | dirs = "3.0" 11 | hotwatch = "0.4.5" 12 | serde = { version = "1.0", features = ["derive"] } 13 | serde_json = "1.0" 14 | serde-xml-rs = "0.4" 15 | libloading = "0.7" 16 | winapi = "0.3.9" 17 | libc = "0.2" 18 | ctrlc = "3.0" 19 | log = "0.4.0" 20 | env_logger = "0.8.3" 21 | toml = "0.5" 22 | glob = "0.3.0" 23 | enum-iterator = "0.7.0" 24 | winreg = "0.10" 25 | 26 | [build-dependencies] 27 | embed-resource = "1.6" 28 | -------------------------------------------------------------------------------- /Changes.md: -------------------------------------------------------------------------------- 1 | # Changes 2 | 3 | ## Version 1.13 4 | 5 | - Add option to configuration file to specify location of bindings file 6 | 7 | ## Version 1.12 8 | 9 | - Automatically detect location of driver files 10 | 11 | ## Version 1.11 12 | 13 | - Do not show FSD as active when in supercruise 14 | 15 | ## Version 1.10 16 | 17 | - Supports active state for night vision control 18 | - Supports alternative (optional) configuration when night vision on 19 | - Supports all combinations of two colours for flashing light modes in 20 | configuration 21 | - Change default active light mode to `amber-flash` to make this state more 22 | noticeable 23 | - Supports specifying the configuration file as a command line argument 24 | - Make alternative hardpoints deployed configuration optional in the 25 | configuration file; falls back to default configuration from the file if 26 | missing 27 | 28 | ## Version 1.9 29 | 30 | - Supports controls bound to the POV2 hat and throttle 31 | - Shows alert on throttle control when speeding 32 | - Sets all lights to the inactive state even if the are not mapped to recognised 33 | controls; this is noticable for example when hardpoints are deployed using the 34 | default configuration 35 | - Keep boolean lights on when inactive by default as this gives a better 36 | experience with both the Fire button and the throttle 37 | - Prevent hardpoints deployed being the global state when in supercruise; this 38 | could be triggered previously by using the FSS 39 | 40 | ## Version 1.8 41 | 42 | - Supports alternative configuration when hardpoints are deployed 43 | - **Important**: Old configuration files are now invalid; see README for 44 | details on the new format; you can delete or rename your old file and let 45 | the app create a new, valid file 46 | - Shows blocked state for FSD and boost when landing gear deployed 47 | - Correctly shows alert state for FSD when the FSD is charging and the ship is 48 | both in supercruise and overheating (already worked properly in normal flight) 49 | 50 | ## Version 1.7 51 | 52 | - Supports `off` mode for red/amber/green lights in configuration 53 | - Supports the Fire button and its boolean light 54 | - Supports both boolean and red/amber/green light modes in configuration - 55 | **Important**: Old configuration files are now invalid; see README for 56 | details on the new format; you can delete or rename your old file and let 57 | the app create a new, valid file 58 | - Shows active state for hardpoints when deployed and inactive state when not 59 | deployed 60 | - Shows active state for FSD when in supercruise 61 | - Shows blocked state for FSD when hardpoints are deployed (unless in 62 | supercruise) 63 | 64 | ## Version 1.6 65 | 66 | - Reads control bindings from Odyssey `Custom.4.0.binds` file 67 | - Reads configuration from `edxlc.toml` file in the current working directory 68 | - Writes default `edxlc.toml` configuration file if missing 69 | - Shows alert state for landing gear if not deployed while docking 70 | 71 | ## Version 1.5 72 | 73 | - Shows active state (yellow) on silent running buttons 74 | - Shows alert state (red/yellow flashing) on FSD and silent running buttons if 75 | that control is active but ship is also overheating 76 | - Shows correct states immediately, i.e. do not wait for first significant state 77 | change before updating. 78 | - Shows correct states consitently on buttons where one LED represents multiple 79 | bound controls, e.g. on T1 and T2 80 | 81 | ## Version 1.4 82 | 83 | - App icon! 84 | - Shows alert state (red/yellow flashing) on heat sink button when overheating 85 | - Silences most output but this can be re-enabled by setting the `RUST_LOG` 86 | environment variable to `edxlc=debug` prior to execution 87 | 88 | ## Version 1.3 89 | 90 | - Shows active state (yellow) on all hyperspace and supercruise related buttons 91 | when FSD is charging 92 | - Shows blocked state (red) on all hyperspace and supercruise related buttons 93 | when mass-locked or during FSD cooldown 94 | - Correctly displays an active state (yellow) where one LED represents multiple 95 | bound controls, e.g. on T1 and T2 96 | 97 | ## Version 1.2 98 | 99 | - Reads button bindings from file `Custom.3.0.binds` so that landing gear, 100 | cargo scoop, and external light states are displayed on the correct, user 101 | configured buttons 102 | - Adds Clutch, Fire A, Fire B, Fire D, Fire E, T1, T3 and T5 to the supported 103 | buttons 104 | 105 | ## Version 1.1 106 | 107 | - Sets T3/T4 button yellow when ship cargo scoop lowered 108 | - Sets T5/T6 button yellow when ship external lights on 109 | - Displays version number on startup 110 | - Exits immediately on Ctrl+C 111 | 112 | ## Version 1.0 113 | 114 | - Sets T1/2 button yellow when ship landing gear deployed 115 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright © 2021 Andrew Smith 2 | 3 | This program is free software: you can redistribute it and/or modify 4 | it under the terms of the GNU General Public License as published by 5 | the Free Software Foundation, either version 3 of the License, or 6 | (at your option) any later version. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU General Public License for more details. 12 | 13 | You should have received a copy of the GNU General Public License 14 | along with this program. If not, see . -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Elite Dangerous X52 Pro Light Control - EDXLC 2 | 3 | **EDXLC** is a companion app for the game **Elite Dangerous** that controls the 4 | button lights on your **Saitek X52 Pro** joystick so that they reflect the 5 | current state of your ship in the game. 6 | 7 | Download [the latest zip file][download], unzip, and run `edxlc.exe`. The app 8 | runs in a console window. Exit with `Ctrl`+`C` or just close the console 9 | window. 10 | 11 | [download]: https://github.com/andrewdsmith/edxlc/releases/download/v1.12/edxlc_v1.12.zip 12 | 13 | On first run the app creates a simple text file called `edxlc.toml`. Edit this 14 | file to change how the app behaves. Restart the app after making any changes to 15 | this file. To reset to defaults delete the file. 16 | 17 | The app detects the following: 18 | 19 | - Landing gear deployed 20 | - Cargo scoop open 21 | - External lights activated 22 | - Frame shift drive (FSD) charging 23 | - Silent running activated 24 | - Hardpoints deployed 25 | - Night vision activated 26 | 27 | Where a game control is bound in game to a button on the joystick, the button 28 | light will indicate the following states: 29 | 30 | - Inactive - Green (Off) - Not currently activated but can be activated 31 | - Active - Amber (On) - Currently activated 32 | - Blocked - Red (Off) - Cannot be activated 33 | - Alert - Flashing amber (Flashing) - May need to be activated urgently 34 | 35 | An example blocked state is FSD charging while mass-locked or landing gear 36 | deployed. Examples of alert states include heat sinks when overheating and 37 | landing gear when docking permission has been granted. 38 | 39 | When hardpoints are deployed or night vision is activated the app switches to 40 | an alternative configuration. 41 | 42 | The default configurations in `edxlc.toml` are: 43 | 44 | ```toml 45 | [default] 46 | inactive = ["off", "green"] 47 | active = ["on", "amber"] 48 | blocked = ["off", "red"] 49 | alert = ["flash", "amber-flash"] 50 | 51 | [hardpoints-deployed] 52 | inactive = ["off", "red"] 53 | active = ["on", "amber"] 54 | blocked = ["off", "off"] 55 | alert = ["flash", "amber-flash"] 56 | 57 | [night-vision] 58 | inactive = ["off", "off"] 59 | active = ["on", "green"] 60 | blocked = ["off", "off"] 61 | alert = ["flash", "green-flash"] 62 | ``` 63 | 64 | The `hardpoints-deployed` and `night-vision` sections are optional and will 65 | fall back to the values in `default` if missing. 66 | 67 | For each state you specify the light mode for boolean and red/amber/green 68 | lights. For boolean lights, the supported modes are: 69 | 70 | - `off` 71 | - `on` 72 | - `flash` 73 | 74 | For red/amber/green ligths, the supported modes are: 75 | 76 | - `off` 77 | - `red` 78 | - `amber` 79 | - `green` 80 | - `red-flash` 81 | - `red-amber-flash` 82 | - `red-green-flash` 83 | - `amber-flash` 84 | - `amber-green-flash` 85 | - `amber-red-flash` 86 | - `green-flash` 87 | - `green-amber-flash` 88 | - `green-red-flash` 89 | 90 | To use an alternative configuration file specify it as a command line argument: 91 | 92 | ``` 93 | edxlc.exe C:\Path\To\My\config.toml 94 | ``` 95 | 96 | By default the app reads the game control bindings from the `Custom.4.0.binds` 97 | bindings file used by Odyssey. You can use a different bindings file (e.g. 98 | `Custom.3.0.binds` for Horizons) by specifying the full path to the file in the 99 | `bindings` value in the `[files]` section of `config.toml`, e.g. 100 | 101 | ```toml 102 | [files] 103 | bindings = 'C:\Users\DavidB\AppData\Local\Frontier Developments\Elite Dangerous\Options\Bindings\Custom.3.0.binds' 104 | ``` 105 | 106 | Important: Due to the way the TOML file format works, you should use single 107 | quote characters around the path (as shown above). -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | extern crate embed_resource; 2 | 3 | fn main() { 4 | embed_resource::compile("src/resources/edxlc.rc"); 5 | } 6 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use crate::game::GlobalStatus; 2 | use crate::x52pro::{ 3 | device::{BooleanLightMode, LightMode, RedAmberGreenLightMode}, 4 | StatusLevelToModeMapper, 5 | }; 6 | use log::info; 7 | use serde::{Deserialize, Serialize}; 8 | use std::{ 9 | fs, 10 | path::{Path, PathBuf}, 11 | }; 12 | 13 | /// Raw configuration string values (as read from a configuraiton file) for a specific game mode. 14 | #[derive(Debug, Deserialize, PartialEq, Serialize)] 15 | struct ModeConfig { 16 | inactive: (BooleanLightMode, RedAmberGreenLightMode), 17 | active: (BooleanLightMode, RedAmberGreenLightMode), 18 | blocked: (BooleanLightMode, RedAmberGreenLightMode), 19 | alert: (BooleanLightMode, RedAmberGreenLightMode), 20 | } 21 | 22 | /// Modal configurations as read from a configuration file. 23 | #[derive(Debug, Deserialize, PartialEq, Serialize)] 24 | #[serde(rename_all = "kebab-case")] 25 | pub struct Config { 26 | files: Option, 27 | default: ModeConfig, 28 | hardpoints_deployed: Option, 29 | night_vision: Option, 30 | } 31 | 32 | #[derive(Debug, Deserialize, PartialEq, Serialize)] 33 | struct Files { 34 | bindings: Option, 35 | } 36 | 37 | const DEFAULT_BINDINGS_FILE_PATH: &str = 38 | r"Frontier Developments\Elite Dangerous\Options\Bindings\Custom.4.0.binds"; 39 | 40 | impl Config { 41 | /// Returns a new instance constructed by loading the give configuration 42 | /// file. Panics if the TOML cannot be parsed. 43 | pub fn from_file(config_filename: String) -> Self { 44 | let toml = fs::read_to_string(config_filename).expect("Could not read configuration file"); 45 | Self::from_toml(&toml) 46 | } 47 | 48 | /// Returns a new instance constructed from the referenced TOML `String`. 49 | /// Panics if the TOML cannot be parsed. 50 | fn from_toml(toml: &String) -> Self { 51 | toml::from_str(&toml).expect("Could not load configuration") 52 | } 53 | 54 | /// Returns a `StatusLevelToModeMapper` for the given `GlobalStatus` value, 55 | /// as configured from the mapped raw string values held by the instance. 56 | pub fn status_level_to_mode_mapper( 57 | &self, 58 | global_status: GlobalStatus, 59 | ) -> StatusLevelToModeMapper { 60 | let mode_config = match global_status { 61 | GlobalStatus::Normal => &self.default, 62 | GlobalStatus::HardpointsDeployed => { 63 | self.mode_config_or_default(&self.hardpoints_deployed) 64 | } 65 | GlobalStatus::NightVisionOn => self.mode_config_or_default(&self.night_vision), 66 | }; 67 | 68 | StatusLevelToModeMapper::new( 69 | light_mode_from_config_values(mode_config.inactive), 70 | light_mode_from_config_values(mode_config.active), 71 | light_mode_from_config_values(mode_config.blocked), 72 | light_mode_from_config_values(mode_config.alert), 73 | ) 74 | } 75 | 76 | fn mode_config_or_default<'a>( 77 | &'a self, 78 | optional_config: &'a Option, 79 | ) -> &'a ModeConfig { 80 | match optional_config { 81 | Some(ref mode_config) => mode_config, 82 | None => &self.default, 83 | } 84 | } 85 | 86 | /// Returns the configured path for the bindings file or the default if not 87 | /// configured. 88 | pub fn bindings_file_path(&self) -> PathBuf { 89 | if let Some(files) = &self.files { 90 | if let Some(bindings) = &files.bindings { 91 | return PathBuf::from(bindings); 92 | } 93 | } 94 | 95 | dirs::data_local_dir() 96 | .expect("Can't find user app data directory") 97 | .join(DEFAULT_BINDINGS_FILE_PATH) 98 | } 99 | } 100 | 101 | /// Returns the `LightMode` value corresponding to the mode tuple. 102 | fn light_mode_from_config_values(value: (BooleanLightMode, RedAmberGreenLightMode)) -> LightMode { 103 | let (boolean, red_amber_green) = value; 104 | LightMode::new(boolean, red_amber_green) 105 | } 106 | 107 | /// Writes a default configuration file to the given filename if that file does 108 | /// not exist. Panics if the file cannot be written, e.g. if the user does not 109 | /// have permission. 110 | pub fn write_default_file_if_missing(config_filename: &str) { 111 | if Path::new(config_filename).exists() { 112 | return; 113 | } 114 | 115 | info!("Writing default configuration file"); 116 | 117 | let config = Config { 118 | files: None, 119 | default: ModeConfig { 120 | inactive: (BooleanLightMode::On, RedAmberGreenLightMode::Green), 121 | active: (BooleanLightMode::On, RedAmberGreenLightMode::Amber), 122 | blocked: (BooleanLightMode::Off, RedAmberGreenLightMode::Red), 123 | alert: (BooleanLightMode::Flash, RedAmberGreenLightMode::AmberFlash), 124 | }, 125 | hardpoints_deployed: Some(ModeConfig { 126 | inactive: (BooleanLightMode::On, RedAmberGreenLightMode::Red), 127 | active: (BooleanLightMode::On, RedAmberGreenLightMode::Amber), 128 | blocked: (BooleanLightMode::Off, RedAmberGreenLightMode::Off), 129 | alert: (BooleanLightMode::Flash, RedAmberGreenLightMode::AmberFlash), 130 | }), 131 | night_vision: Some(ModeConfig { 132 | inactive: (BooleanLightMode::Off, RedAmberGreenLightMode::Off), 133 | active: (BooleanLightMode::On, RedAmberGreenLightMode::Green), 134 | blocked: (BooleanLightMode::Off, RedAmberGreenLightMode::Off), 135 | alert: (BooleanLightMode::Flash, RedAmberGreenLightMode::GreenFlash), 136 | }), 137 | }; 138 | 139 | let toml = toml::to_string(&config).expect("Could not serialize default configuration"); 140 | fs::write(config_filename, toml).expect("Could not write default configuration file"); 141 | } 142 | 143 | #[cfg(test)] 144 | mod tests { 145 | use super::*; 146 | 147 | #[test] 148 | fn config_from_toml_returns_an_instance() { 149 | let toml = r#" 150 | [files] 151 | bindings = 'C:\Path\To.binds' 152 | [default] 153 | inactive = ["off", "green"] 154 | active = ["on", "amber"] 155 | blocked = ["on", "red"] 156 | alert = ["flash", "red-amber"] 157 | [hardpoints-deployed] 158 | inactive = ["on", "green"] 159 | active = ["off", "amber"] 160 | blocked = ["flash", "red"] 161 | alert = ["off", "red-amber"] 162 | [night-vision] 163 | inactive = ["flash", "green"] 164 | active = ["flash", "amber"] 165 | blocked = ["off", "red"] 166 | alert = ["on", "red-amber"]"#; 167 | 168 | let expected = Config { 169 | files: Some(Files { 170 | bindings: Some(String::from(r"C:\Path\To.binds")), 171 | }), 172 | default: ModeConfig { 173 | inactive: (BooleanLightMode::Off, RedAmberGreenLightMode::Green), 174 | active: (BooleanLightMode::On, RedAmberGreenLightMode::Amber), 175 | blocked: (BooleanLightMode::On, RedAmberGreenLightMode::Red), 176 | alert: (BooleanLightMode::Flash, RedAmberGreenLightMode::RedAmber), 177 | }, 178 | hardpoints_deployed: Some(ModeConfig { 179 | inactive: (BooleanLightMode::On, RedAmberGreenLightMode::Green), 180 | active: (BooleanLightMode::Off, RedAmberGreenLightMode::Amber), 181 | blocked: (BooleanLightMode::Flash, RedAmberGreenLightMode::Red), 182 | alert: (BooleanLightMode::Off, RedAmberGreenLightMode::RedAmber), 183 | }), 184 | night_vision: Some(ModeConfig { 185 | inactive: (BooleanLightMode::Flash, RedAmberGreenLightMode::Green), 186 | active: (BooleanLightMode::Flash, RedAmberGreenLightMode::Amber), 187 | blocked: (BooleanLightMode::Off, RedAmberGreenLightMode::Red), 188 | alert: (BooleanLightMode::On, RedAmberGreenLightMode::RedAmber), 189 | }), 190 | }; 191 | 192 | assert_eq!(Config::from_toml(&String::from(toml)), expected); 193 | } 194 | 195 | #[test] 196 | fn config_from_toml_returns_an_instance_with_none_for_missing_blocks() { 197 | let toml = r#" 198 | [default] 199 | inactive = ["off", "green"] 200 | active = ["on", "amber"] 201 | blocked = ["on", "red"] 202 | alert = ["flash", "red-amber"]"#; 203 | 204 | let expected = Config { 205 | files: None, 206 | default: ModeConfig { 207 | inactive: (BooleanLightMode::Off, RedAmberGreenLightMode::Green), 208 | active: (BooleanLightMode::On, RedAmberGreenLightMode::Amber), 209 | blocked: (BooleanLightMode::On, RedAmberGreenLightMode::Red), 210 | alert: (BooleanLightMode::Flash, RedAmberGreenLightMode::RedAmber), 211 | }, 212 | hardpoints_deployed: None, 213 | night_vision: None, 214 | }; 215 | 216 | assert_eq!(Config::from_toml(&String::from(toml)), expected); 217 | } 218 | 219 | #[test] 220 | fn config_status_level_to_mode_mapper_returns_configured_mapped() { 221 | let default_light_config = (BooleanLightMode::On, RedAmberGreenLightMode::Green); 222 | let other_light_config = (BooleanLightMode::Off, RedAmberGreenLightMode::Red); 223 | 224 | let default_light_mode = LightMode { 225 | boolean: default_light_config.0, 226 | red_amber_green: default_light_config.1, 227 | }; 228 | 229 | let config = Config { 230 | files: None, 231 | default: ModeConfig { 232 | inactive: default_light_config, 233 | active: default_light_config, 234 | blocked: default_light_config, 235 | alert: default_light_config, 236 | }, 237 | hardpoints_deployed: Some(ModeConfig { 238 | inactive: other_light_config, 239 | active: other_light_config, 240 | blocked: other_light_config, 241 | alert: other_light_config, 242 | }), 243 | night_vision: None, 244 | }; 245 | 246 | let actual_mapper = config.status_level_to_mode_mapper(GlobalStatus::Normal); 247 | let expected_mapper = StatusLevelToModeMapper { 248 | inactive: default_light_mode, 249 | active: default_light_mode, 250 | blocked: default_light_mode, 251 | alert: default_light_mode, 252 | }; 253 | 254 | assert_eq!(actual_mapper, expected_mapper); 255 | } 256 | 257 | #[test] 258 | fn config_status_level_to_mode_mapper_returns_night_vision_mapper() { 259 | let default_light_config = (BooleanLightMode::On, RedAmberGreenLightMode::Green); 260 | let night_vision_light_config = (BooleanLightMode::Off, RedAmberGreenLightMode::Off); 261 | 262 | let config = Config { 263 | files: None, 264 | default: ModeConfig { 265 | inactive: default_light_config, 266 | active: default_light_config, 267 | blocked: default_light_config, 268 | alert: default_light_config, 269 | }, 270 | hardpoints_deployed: None, 271 | night_vision: Some(ModeConfig { 272 | inactive: night_vision_light_config, 273 | active: night_vision_light_config, 274 | blocked: night_vision_light_config, 275 | alert: night_vision_light_config, 276 | }), 277 | }; 278 | 279 | let actual_mapper = config.status_level_to_mode_mapper(GlobalStatus::NightVisionOn); 280 | 281 | let expected_light_mode = LightMode { 282 | boolean: night_vision_light_config.0, 283 | red_amber_green: night_vision_light_config.1, 284 | }; 285 | let expected_mapper = StatusLevelToModeMapper { 286 | inactive: expected_light_mode, 287 | active: expected_light_mode, 288 | blocked: expected_light_mode, 289 | alert: expected_light_mode, 290 | }; 291 | 292 | assert_eq!(actual_mapper, expected_mapper); 293 | } 294 | 295 | #[test] 296 | fn config_status_level_to_mode_mapper_returns_defaults() { 297 | let default_light_config = (BooleanLightMode::On, RedAmberGreenLightMode::Green); 298 | 299 | let default_light_mode = LightMode { 300 | boolean: default_light_config.0, 301 | red_amber_green: default_light_config.1, 302 | }; 303 | 304 | // Could remove the `None` values by implementing `Default` on the 305 | // struct. 306 | let config_without_hardpoints_deployed = Config { 307 | files: None, 308 | default: ModeConfig { 309 | inactive: default_light_config, 310 | active: default_light_config, 311 | blocked: default_light_config, 312 | alert: default_light_config, 313 | }, 314 | hardpoints_deployed: None, 315 | night_vision: None, 316 | }; 317 | 318 | let expected_mapper = StatusLevelToModeMapper { 319 | inactive: default_light_mode, 320 | active: default_light_mode, 321 | blocked: default_light_mode, 322 | alert: default_light_mode, 323 | }; 324 | 325 | let global_statuses = vec![ 326 | GlobalStatus::Normal, 327 | GlobalStatus::HardpointsDeployed, 328 | GlobalStatus::NightVisionOn, 329 | ]; 330 | 331 | for global_status in global_statuses { 332 | let actual_mapper = 333 | config_without_hardpoints_deployed.status_level_to_mode_mapper(global_status); 334 | 335 | assert_eq!(actual_mapper, expected_mapper); 336 | } 337 | } 338 | } 339 | -------------------------------------------------------------------------------- /src/events.rs: -------------------------------------------------------------------------------- 1 | use crate::game::file::Status; 2 | use std::path::PathBuf; 3 | 4 | #[derive(Debug, PartialEq)] 5 | pub enum Event { 6 | NewJournalFile(PathBuf), 7 | AnimationTick, 8 | StatusUpdate(Status), 9 | Exit, 10 | } 11 | -------------------------------------------------------------------------------- /src/game.rs: -------------------------------------------------------------------------------- 1 | mod controls; 2 | pub mod file; 3 | mod ship; 4 | 5 | pub use controls::*; 6 | pub use ship::*; 7 | -------------------------------------------------------------------------------- /src/game/controls.rs: -------------------------------------------------------------------------------- 1 | use crate::game::file::{ControlBindings, Input as BindingsInput}; 2 | use crate::x52pro::device::Input; 3 | use std::path::PathBuf; 4 | 5 | const X52PRO_DEVICE: &str = "SaitekX52Pro"; 6 | const X52PRO_CLUTCH: &str = "Joy_31"; 7 | const X52PRO_FIRE: &str = "Joy_2"; 8 | const X52PRO_FIRE_A: &str = "Joy_3"; 9 | const X52PRO_FIRE_B: &str = "Joy_4"; 10 | const X52PRO_FIRE_D: &str = "Joy_7"; 11 | const X52PRO_FIRE_E: &str = "Joy_8"; 12 | const X52PRO_POV2_DOWN: &str = "Joy_22"; 13 | const X52PRO_POV2_LEFT: &str = "Joy_23"; 14 | const X52PRO_POV2_RIGHT: &str = "Joy_21"; 15 | const X52PRO_POV2_UP: &str = "Joy_20"; 16 | const X52PRO_T1: &str = "Joy_9"; 17 | const X52PRO_T2: &str = "Joy_10"; 18 | const X52PRO_T3: &str = "Joy_11"; 19 | const X52PRO_T4: &str = "Joy_12"; 20 | const X52PRO_T5: &str = "Joy_13"; 21 | const X52PRO_T6: &str = "Joy_14"; 22 | const X52PRO_Z_AXIS: &str = "Joy_ZAxis"; 23 | 24 | /// A supported game control that can be mapped to an X52Pro input. 25 | pub enum Control { 26 | Boost, 27 | CargoScoop, 28 | ExternalLights, 29 | Hardpoints, 30 | HeatSink, 31 | Hyperspace, 32 | HyperSuperCombination, 33 | LandingGear, 34 | NightVision, 35 | SilentRunning, 36 | Supercruise, 37 | Throttle, 38 | } 39 | 40 | /// The set of game controls bound to X52Pro inputs as loaded from a bindings 41 | /// file. 42 | #[derive(Debug)] 43 | pub struct Controls { 44 | file: ControlBindings, 45 | } 46 | 47 | impl Controls { 48 | /// Returns an instance built by loaded the bindings file at the give path. 49 | pub fn from_file(path: &PathBuf) -> Self { 50 | Self::from_file_control_bindings(ControlBindings::from_file(path)) 51 | } 52 | 53 | /// Returns an instance built from the given `ControlBindings` instance. 54 | pub fn from_file_control_bindings(file: ControlBindings) -> Self { 55 | Controls { file } 56 | } 57 | 58 | /// Returns a vector containing all the `Input` instances that are bound to 59 | /// the given `Control` instance. The vector will be empty if none of the 60 | /// supported inputs is bound to the given control. 61 | pub fn inputs_for_control(&self, control: Control) -> Vec { 62 | let control_binding = match control { 63 | Control::Boost => &self.file.boost, 64 | Control::CargoScoop => &self.file.cargo_scoop, 65 | Control::ExternalLights => &self.file.external_lights, 66 | Control::Hardpoints => &self.file.hardpoints, 67 | Control::HeatSink => &self.file.heat_sink, 68 | Control::Hyperspace => &self.file.hyperspace, 69 | Control::HyperSuperCombination => &self.file.hyper_super_combo, 70 | Control::LandingGear => &self.file.landing_gear, 71 | Control::NightVision => &self.file.night_vision, 72 | Control::SilentRunning => &self.file.silent_running, 73 | Control::Supercruise => &self.file.supercruise, 74 | Control::Throttle => &self.file.throttle, 75 | }; 76 | 77 | let mut inputs = Vec::with_capacity(2); 78 | 79 | // This could probably be more elegantly written by mapping the vector 80 | // elements through the function and collecting the non-None elements. 81 | for file_input in vec![ 82 | &control_binding.primary, 83 | &control_binding.secondary, 84 | &control_binding.binding, 85 | ] { 86 | if let Some(input) = input_from_file_input(file_input) { 87 | inputs.push(input); 88 | } 89 | } 90 | 91 | inputs 92 | } 93 | } 94 | 95 | /// Returns a supported X52Pro `Input` that matches the bindings file input. 96 | fn input_from_file_input(input: &BindingsInput) -> Option { 97 | match input.device.as_str() { 98 | X52PRO_DEVICE => match input.name.as_str() { 99 | X52PRO_CLUTCH => Some(Input::Clutch), 100 | X52PRO_FIRE => Some(Input::Fire), 101 | X52PRO_FIRE_A => Some(Input::FireA), 102 | X52PRO_FIRE_B => Some(Input::FireB), 103 | X52PRO_FIRE_D => Some(Input::FireD), 104 | X52PRO_FIRE_E => Some(Input::FireE), 105 | X52PRO_POV2_DOWN => Some(Input::PoV2Down), 106 | X52PRO_POV2_LEFT => Some(Input::PoV2Left), 107 | X52PRO_POV2_RIGHT => Some(Input::PoV2Right), 108 | X52PRO_POV2_UP => Some(Input::PoV2Up), 109 | X52PRO_T1 => Some(Input::T1), 110 | X52PRO_T2 => Some(Input::T2), 111 | X52PRO_T3 => Some(Input::T3), 112 | X52PRO_T4 => Some(Input::T4), 113 | X52PRO_T5 => Some(Input::T5), 114 | X52PRO_T6 => Some(Input::T6), 115 | X52PRO_Z_AXIS => Some(Input::ZAxis), 116 | _ => None, 117 | }, 118 | _ => None, 119 | } 120 | } 121 | 122 | #[cfg(test)] 123 | mod tests { 124 | use super::*; 125 | use crate::game::file::ControlBinding; 126 | 127 | #[test] 128 | fn controls_inputs_for_control() { 129 | let file_control_bindings = ControlBindings { 130 | cargo_scoop: ControlBinding::new((X52PRO_DEVICE, X52PRO_T2), ("", "")), 131 | external_lights: ControlBinding::new(("", ""), (X52PRO_DEVICE, X52PRO_T4)), 132 | landing_gear: ControlBinding::new( 133 | (X52PRO_DEVICE, X52PRO_T2), 134 | (X52PRO_DEVICE, X52PRO_T4), 135 | ), 136 | hyper_super_combo: ControlBinding::new((X52PRO_DEVICE, X52PRO_T1), ("", "")), 137 | supercruise: ControlBinding::new((X52PRO_DEVICE, X52PRO_T3), ("", "")), 138 | hyperspace: ControlBinding::new((X52PRO_DEVICE, X52PRO_T5), ("", "")), 139 | silent_running: ControlBinding::new((X52PRO_DEVICE, X52PRO_FIRE_A), ("", "")), 140 | heat_sink: ControlBinding::new((X52PRO_DEVICE, X52PRO_T6), ("", "")), 141 | hardpoints: ControlBinding::new((X52PRO_DEVICE, X52PRO_FIRE_B), ("", "")), 142 | boost: ControlBinding::new((X52PRO_DEVICE, X52PRO_FIRE_D), ("", "")), 143 | throttle: ControlBinding::new((X52PRO_DEVICE, X52PRO_FIRE_E), ("", "")), 144 | night_vision: ControlBinding::new((X52PRO_DEVICE, X52PRO_POV2_DOWN), ("", "")), 145 | }; 146 | let controls = Controls::from_file_control_bindings(file_control_bindings); 147 | 148 | assert_eq!( 149 | controls.inputs_for_control(Control::CargoScoop), 150 | vec![Input::T2] 151 | ); 152 | assert_eq!( 153 | controls.inputs_for_control(Control::ExternalLights), 154 | vec![Input::T4] 155 | ); 156 | assert_eq!( 157 | controls.inputs_for_control(Control::LandingGear), 158 | vec![Input::T2, Input::T4] 159 | ); 160 | assert_eq!( 161 | controls.inputs_for_control(Control::HyperSuperCombination), 162 | vec![Input::T1] 163 | ); 164 | assert_eq!( 165 | controls.inputs_for_control(Control::Supercruise), 166 | vec![Input::T3] 167 | ); 168 | assert_eq!( 169 | controls.inputs_for_control(Control::Hyperspace), 170 | vec![Input::T5] 171 | ); 172 | assert_eq!( 173 | controls.inputs_for_control(Control::SilentRunning), 174 | vec![Input::FireA] 175 | ); 176 | assert_eq!( 177 | controls.inputs_for_control(Control::HeatSink), 178 | vec![Input::T6] 179 | ); 180 | assert_eq!( 181 | controls.inputs_for_control(Control::Hardpoints), 182 | vec![Input::FireB] 183 | ); 184 | assert_eq!( 185 | controls.inputs_for_control(Control::Boost), 186 | vec![Input::FireD] 187 | ); 188 | assert_eq!( 189 | controls.inputs_for_control(Control::NightVision), 190 | vec![Input::PoV2Down] 191 | ); 192 | } 193 | 194 | #[test] 195 | fn input_from_file_input_returns_optional_inputs_given_a_file_input() { 196 | fn call_with(device: &str, name: &str) -> Option { 197 | input_from_file_input(&BindingsInput::new(device, name)) 198 | } 199 | 200 | assert_eq!(call_with(X52PRO_DEVICE, X52PRO_CLUTCH), Some(Input::Clutch)); 201 | assert_eq!(call_with(X52PRO_DEVICE, X52PRO_FIRE_A), Some(Input::FireA)); 202 | assert_eq!(call_with(X52PRO_DEVICE, X52PRO_FIRE_B), Some(Input::FireB)); 203 | assert_eq!(call_with(X52PRO_DEVICE, X52PRO_FIRE_D), Some(Input::FireD)); 204 | assert_eq!(call_with(X52PRO_DEVICE, X52PRO_FIRE_E), Some(Input::FireE)); 205 | assert_eq!( 206 | call_with(X52PRO_DEVICE, X52PRO_POV2_DOWN), 207 | Some(Input::PoV2Down) 208 | ); 209 | assert_eq!( 210 | call_with(X52PRO_DEVICE, X52PRO_POV2_LEFT), 211 | Some(Input::PoV2Left) 212 | ); 213 | assert_eq!( 214 | call_with(X52PRO_DEVICE, X52PRO_POV2_RIGHT), 215 | Some(Input::PoV2Right) 216 | ); 217 | assert_eq!( 218 | call_with(X52PRO_DEVICE, X52PRO_POV2_UP), 219 | Some(Input::PoV2Up) 220 | ); 221 | assert_eq!(call_with(X52PRO_DEVICE, X52PRO_T1), Some(Input::T1)); 222 | assert_eq!(call_with(X52PRO_DEVICE, X52PRO_T2), Some(Input::T2)); 223 | assert_eq!(call_with(X52PRO_DEVICE, X52PRO_T3), Some(Input::T3)); 224 | assert_eq!(call_with(X52PRO_DEVICE, X52PRO_T4), Some(Input::T4)); 225 | assert_eq!(call_with(X52PRO_DEVICE, X52PRO_T5), Some(Input::T5)); 226 | assert_eq!(call_with(X52PRO_DEVICE, X52PRO_T6), Some(Input::T6)); 227 | assert_eq!(call_with(X52PRO_DEVICE, X52PRO_Z_AXIS), Some(Input::ZAxis)); 228 | assert_eq!(call_with(X52PRO_DEVICE, "Other"), None); 229 | assert_eq!(call_with("Other", X52PRO_T2), None); 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /src/game/file.rs: -------------------------------------------------------------------------------- 1 | mod control_bindings; 2 | pub mod journal; 3 | 4 | pub use control_bindings::*; 5 | use glob::glob; 6 | use log::debug; 7 | use serde::Deserialize; 8 | use std::fs; 9 | use std::path::PathBuf; 10 | 11 | /// Returns a `PathBuf` for the directory containing the game's journal files. 12 | pub fn journal_dir_path() -> PathBuf { 13 | dirs::home_dir() 14 | .expect("Can't find user home directory") 15 | .join(r#"Saved Games\Frontier Developments\Elite Dangerous"#) // TODO const & use below 16 | } 17 | 18 | /// Optionally returns a `PathBuf` for the latest journal file if one is found. 19 | pub fn latest_journal_file_path() -> Option { 20 | let journal_file_pattern = dirs::home_dir() 21 | .expect("Can't find user home directory") 22 | .join(r#"Saved Games\Frontier Developments\Elite Dangerous\Journal*.log"#); 23 | let journal_file_pattern = journal_file_pattern 24 | .to_str() 25 | .expect("Can't convert user home directory to UTF-8"); 26 | 27 | debug!("Journal file pattern: {:?}", journal_file_pattern); 28 | 29 | glob(journal_file_pattern) 30 | .expect("Can't search for journal files") 31 | .filter_map(Result::ok) 32 | .max_by_key(|path| { 33 | path.metadata() 34 | .expect("Can't get journal file metadata") 35 | .modified() 36 | .expect("Can't get journal file modified date") 37 | }) 38 | } 39 | 40 | pub fn status_file_path() -> PathBuf { 41 | dirs::home_dir() 42 | .expect("Can't find user home directory") 43 | .join(r#"Saved Games\Frontier Developments\Elite Dangerous\Status.json"#) 44 | } 45 | 46 | #[derive(Debug, Default, Deserialize, Eq, PartialEq)] 47 | #[serde(default)] 48 | pub struct Status { 49 | #[serde(rename = "Flags")] 50 | pub flags: u32, 51 | #[serde(rename = "LegalState")] 52 | pub legal_state: LegalState, 53 | } 54 | 55 | impl Status { 56 | // Returns the status in the given file. Returns an Option because the file cannot be 57 | // guaranteed to contain a readable status at all times. 58 | pub fn from_file(path: &PathBuf) -> Option { 59 | let json = fs::read_to_string(path).expect("Could not read status file"); 60 | 61 | // When exiting the game temporarily writes an empty file. 62 | if json == "" { 63 | debug!("Status file empty"); 64 | None 65 | } else { 66 | Some(Status::from_json(json)) 67 | } 68 | } 69 | 70 | pub fn from_json(json: String) -> Status { 71 | debug!("Parsing JSON: {}", json); 72 | serde_json::from_str(&json).expect("Could not parse status JSON") 73 | } 74 | } 75 | 76 | #[derive(Debug, Deserialize, Eq, PartialEq)] 77 | pub enum LegalState { 78 | Speeding, 79 | #[serde(other)] 80 | Other, 81 | } 82 | 83 | impl Default for LegalState { 84 | fn default() -> Self { 85 | LegalState::Other 86 | } 87 | } 88 | 89 | #[cfg(test)] 90 | mod tests { 91 | use super::*; 92 | 93 | #[test] 94 | fn status_from_json_parses_flags() { 95 | let json = String::from( 96 | r#"{"timestamp": "2021-08-21T21:36:35Z", "event": "Status", "Flags": 4, "LegalState": "Speeding"}"#, 97 | ); 98 | 99 | assert_eq!( 100 | Status::from_json(json), 101 | Status { 102 | flags: 4, 103 | legal_state: LegalState::Speeding 104 | } 105 | ); 106 | } 107 | 108 | #[test] 109 | fn status_from_json_parses_when_legal_state_missing() { 110 | let json = 111 | String::from(r#"{"timestamp": "2021-08-21T21:36:35Z", "event": "Status", "Flags": 0}"#); 112 | assert_eq!(Status::from_json(json).legal_state, LegalState::Other); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/game/file/control_bindings.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | use std::fs; 3 | use std::path::PathBuf; 4 | 5 | /// A set of mappings from device inputs to game controls as stored in the game 6 | /// binding files. 7 | #[derive(Deserialize, Debug, PartialEq)] 8 | #[serde(rename = "Root")] 9 | pub struct ControlBindings { 10 | #[serde(rename = "ShipSpotLightToggle")] 11 | pub external_lights: ControlBinding, 12 | #[serde(rename = "ToggleCargoScoop")] 13 | pub cargo_scoop: ControlBinding, 14 | #[serde(rename = "LandingGearToggle")] 15 | pub landing_gear: ControlBinding, 16 | #[serde(rename = "DeployHardpointToggle")] 17 | pub hardpoints: ControlBinding, 18 | #[serde(rename = "UseBoostJuice")] 19 | pub boost: ControlBinding, 20 | #[serde(rename = "HyperSuperCombination")] 21 | pub hyper_super_combo: ControlBinding, 22 | #[serde(rename = "Supercruise")] 23 | pub supercruise: ControlBinding, 24 | #[serde(rename = "Hyperspace")] 25 | pub hyperspace: ControlBinding, 26 | #[serde(rename = "ToggleButtonUpInput")] 27 | pub silent_running: ControlBinding, 28 | #[serde(rename = "DeployHeatSink")] 29 | pub heat_sink: ControlBinding, 30 | #[serde(rename = "ThrottleAxis")] 31 | pub throttle: ControlBinding, 32 | #[serde(rename = "NightVisionToggle")] 33 | pub night_vision: ControlBinding, 34 | } 35 | 36 | impl ControlBindings { 37 | pub fn from_file(path: &PathBuf) -> Self { 38 | let xml = fs::read_to_string(path).expect("Could not read bindings file"); 39 | Self::from_str(xml) 40 | } 41 | 42 | pub fn from_str(xml: String) -> Self { 43 | serde_xml_rs::from_str(&xml).expect("Could not parse bindings XML") 44 | } 45 | } 46 | 47 | /// A pair of device inputs that can be mapped to a game control, as stored in 48 | /// the game binding files. 49 | #[derive(Default, Deserialize, Debug, PartialEq)] 50 | #[serde(default)] 51 | pub struct ControlBinding { 52 | #[serde(rename = "Primary")] 53 | pub primary: Input, 54 | #[serde(rename = "Secondary")] 55 | pub secondary: Input, 56 | #[serde(rename = "Binding")] 57 | pub binding: Input, 58 | } 59 | 60 | impl ControlBinding { 61 | #[cfg(test)] 62 | pub fn new(primary: (&str, &str), secondary: (&str, &str)) -> Self { 63 | Self { 64 | primary: Input::new(primary.0, primary.1), 65 | secondary: Input::new(secondary.0, secondary.1), 66 | ..Default::default() 67 | } 68 | } 69 | } 70 | 71 | /// A device input as stored in the game binding files. 72 | #[derive(Default, Deserialize, Debug, PartialEq)] 73 | pub struct Input { 74 | #[serde(rename = "Device")] 75 | pub device: String, 76 | #[serde(rename = "Key")] 77 | pub name: String, 78 | } 79 | 80 | impl Input { 81 | #[cfg(test)] 82 | pub fn new(device: &str, name: &str) -> Self { 83 | Self { 84 | device: String::from(device), 85 | name: String::from(name), 86 | } 87 | } 88 | } 89 | 90 | #[cfg(test)] 91 | mod tests { 92 | use super::*; 93 | 94 | #[test] 95 | fn control_bindings_from_xml() { 96 | let xml = String::from( 97 | r#" 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | "#, 148 | ); 149 | 150 | let expected = ControlBindings { 151 | external_lights: ControlBinding::new(("D1", "K1"), ("D2", "K2")), 152 | cargo_scoop: ControlBinding::new(("D3", "K3"), ("D4", "K4")), 153 | landing_gear: ControlBinding::new(("D5", "K5"), ("D6", "K6")), 154 | hyper_super_combo: ControlBinding::new(("D7", "K7"), ("D8", "K8")), 155 | supercruise: ControlBinding::new(("D9", "K9"), ("D10", "K10")), 156 | hyperspace: ControlBinding::new(("D11", "K11"), ("D12", "K12")), 157 | silent_running: ControlBinding::new(("D13", "K13"), ("D14", "K14")), 158 | heat_sink: ControlBinding::new(("D15", "K15"), ("D16", "K16")), 159 | hardpoints: ControlBinding::new(("D17", "K17"), ("D18", "K18")), 160 | boost: ControlBinding::new(("D19", "K19"), ("D20", "K20")), 161 | throttle: ControlBinding { 162 | binding: Input { 163 | device: String::from("D21"), 164 | name: String::from("K21"), 165 | }, 166 | ..Default::default() 167 | }, 168 | night_vision: ControlBinding::new(("D22", "K22"), ("D23", "K23")), 169 | }; 170 | 171 | assert_eq!(ControlBindings::from_str(xml), expected); 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /src/game/file/journal.rs: -------------------------------------------------------------------------------- 1 | use crate::events; 2 | use hotwatch::Hotwatch; 3 | use log::{debug, info}; 4 | use serde::Deserialize; 5 | use std::{ 6 | fs::File, 7 | io::{BufRead, BufReader}, 8 | path::PathBuf, 9 | sync::mpsc::Sender, 10 | }; 11 | 12 | /// Watch the given directory for new journal files then send a 13 | /// `NewJournalFile` event using the channel sender. 14 | pub fn watch_dir(dir_path: PathBuf, watcher: &mut Hotwatch, tx: &Sender) { 15 | let tx = tx.clone(); 16 | 17 | watcher 18 | .watch(dir_path, move |event: hotwatch::Event| { 19 | debug!("Journal directory watch event: {:?}", event); 20 | 21 | if let hotwatch::Event::Create(file_path) = event { 22 | let file_name = file_path 23 | .file_name() 24 | .expect("Can't get file name for created file") 25 | .to_str() 26 | .expect("Can't convert file name to UTF-8"); 27 | 28 | debug!("New file in journal directory: {}", file_name); 29 | 30 | if file_name.starts_with("Journal") && file_name.ends_with(".log") { 31 | tx.send(events::Event::NewJournalFile(file_path)) 32 | .expect("Can't send new journal file message"); 33 | } 34 | } 35 | }) 36 | .expect("Can't watch journal directory"); 37 | } 38 | 39 | /// A stateful reader that can be called repeatedly, each time returning only 40 | /// the new journal events appended to journal file since the last call. 41 | pub struct JournalReader { 42 | journal_buf_reader: Option>, 43 | } 44 | 45 | impl JournalReader { 46 | /// Returns a new instance of the reader not associated with any journal 47 | /// file. 48 | pub fn new() -> Self { 49 | JournalReader { 50 | journal_buf_reader: None, 51 | } 52 | } 53 | 54 | /// Opens the given file for reading. 55 | pub fn open(&mut self, journal_file_path: PathBuf) { 56 | debug!("Opening journal file: {:?}", journal_file_path); 57 | let journal_file = File::open(&journal_file_path).expect("Can't open journal file"); 58 | self.journal_buf_reader = Some(BufReader::new(journal_file)); 59 | } 60 | 61 | /// When called before `open` returns an empty vector. When called the first 62 | /// time after `open` returns all the journal events currently in the 63 | /// journal. On subsequent calls returns the new journal events appended to 64 | /// journal file since the last call. 65 | pub fn new_events(&mut self) -> Vec { 66 | if let Some(reader) = &mut self.journal_buf_reader { 67 | events_from_buf_reader(reader, event_from_json) 68 | } else { 69 | vec![] 70 | } 71 | } 72 | } 73 | 74 | /// Read lines from the given reader and map to journal events using the given 75 | /// parser, filtering out `Event::Other`. 76 | fn events_from_buf_reader(reader: &mut BufReader, parser: fn(&str) -> Event) -> Vec 77 | where 78 | T: std::io::Read, 79 | { 80 | let mut events = Vec::new(); 81 | let mut line = String::new(); 82 | 83 | while reader 84 | .read_line(&mut line) 85 | .expect("Can't read journal file") 86 | != 0 87 | { 88 | match parser(&line) { 89 | Event::Other => (), 90 | event => { 91 | info!("Journal event {:?}", event); 92 | events.push(event); 93 | } 94 | } 95 | 96 | // The `read_line` call above *appends* to the string but we want to 97 | // parse one line at a time. 98 | line.clear(); 99 | } 100 | 101 | events 102 | } 103 | 104 | /// Returns a journal event parsed from the given JSON string. 105 | fn event_from_json(json: &str) -> Event { 106 | serde_json::from_str(&json).expect("Can't parse journal event JSON") 107 | } 108 | 109 | // This enum should be renamed `JournalEvent` to reduce name collisions outside 110 | // this module (given it's public). 111 | #[derive(Deserialize, Debug, PartialEq)] 112 | #[serde(tag = "event")] 113 | pub enum Event { 114 | Docked, 115 | DockingCancelled, 116 | DockingGranted, 117 | DockingTimeout, 118 | #[serde(other)] 119 | Other, 120 | } 121 | 122 | #[cfg(test)] 123 | mod tests { 124 | use super::*; 125 | 126 | #[test] 127 | fn events_from_buf_reader_maps_each_line_to_an_event() { 128 | let events = "LINE1\nLINE2\n".as_bytes(); 129 | let mut reader = BufReader::new(events); 130 | fn fake_parser(json: &str) -> Event { 131 | match json { 132 | "LINE1\n" => Event::DockingGranted, 133 | "LINE2\n" => Event::Other, 134 | _ => panic!("Unexpected line value passed to parser '{}'", json), 135 | } 136 | } 137 | 138 | // Filters out `Event::Other`. 139 | assert_eq!( 140 | events_from_buf_reader(&mut reader, fake_parser), 141 | vec![Event::DockingGranted] 142 | ); 143 | } 144 | 145 | #[test] 146 | fn event_from_json_returns_parsed_journal_events() { 147 | assert_eq!( 148 | event_from_json( 149 | r#"{ "timestamp":"2021-05-14T00:00:00Z", "event":"Docked", "StationName":"A", "StationType":"B", "StarSystem":"C", "SystemAddress":1, "MarketID":2, "StationFaction":{ "Name":"D" }, "StationGovernment":"E", "StationGovernment_Localised":"F", "StationAllegiance":"G", "StationServices":[ "H" ], "StationEconomy":"I", "StationEconomy_Localised":"J", "StationEconomies":[ { "Name":"K", "Name_Localised":"L", "Proportion":3.0 } ], "DistFromStarLS":4.0 }"# 150 | ), 151 | Event::Docked 152 | ); 153 | assert_eq!( 154 | event_from_json( 155 | r#"{ "timestamp":"2021-05-13T00:00:00Z", "event":"DockingCancelled", "MarketID":1, "StationName":"A", "StationType":"B" }"# 156 | ), 157 | Event::DockingCancelled 158 | ); 159 | assert_eq!( 160 | event_from_json( 161 | r#"{ "timestamp":"2021-05-12T00:00:00Z", "event":"DockingGranted", "LandingPad":1, "MarketID":1, "StationName":"A", "StationType":"B" }"# 162 | ), 163 | Event::DockingGranted 164 | ); 165 | assert_eq!( 166 | event_from_json( 167 | r#"{ "timestamp":"2021-05-14T00:00:00Z", "event":"DockingTimeout", "MarketID":1, "StationName":"A", "StationType":"B" }"# 168 | ), 169 | Event::DockingTimeout 170 | ); 171 | assert_eq!( 172 | event_from_json( 173 | r#"{ "timestamp":"2021-05-12T00:00:00Z", "event":"Music", "MusicTrack":"NoTrack" }"# 174 | ), 175 | Event::Other 176 | ); 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/game/ship.rs: -------------------------------------------------------------------------------- 1 | use super::file::{journal::Event, LegalState, Status as FileStatus}; 2 | use log::{info, warn}; 3 | 4 | type StatusBitField = u64; 5 | 6 | // See: https://elite-journal.readthedocs.io/en/latest/Status%20File/ 7 | const LANDING_GEAR_DEPLOYED: StatusBitField = 1 << 2; 8 | const SUPERCRUISE: StatusBitField = 1 << 4; 9 | const HARDPOINTS_DEPLOYED: StatusBitField = 1 << 6; 10 | const EXTERNAL_LIGHTS_ON: StatusBitField = 1 << 8; 11 | const CARGO_SCOOP_DEPLOYED: StatusBitField = 1 << 9; 12 | const SILENT_RUNNING: StatusBitField = 1 << 10; 13 | const MASS_LOCKED: StatusBitField = 1 << 16; 14 | const FRAME_SHIFT_DRIVE_CHARGING: StatusBitField = 1 << 17; 15 | const FRAME_SHIFT_DRIVE_COOLDOWN: StatusBitField = 1 << 18; 16 | const OVERHEATING: StatusBitField = 1 << 20; 17 | const NIGHT_VISION_ON: StatusBitField = 1 << 28; 18 | 19 | // These statuses are derived from sources other than the flag fields (e.g. 20 | // legal status and journal events) so we pack them into the unused high bits. 21 | const DOCKING: StatusBitField = 1 << (32 + 16); 22 | const SPEEDING: StatusBitField = 1 << (32 + 17); 23 | 24 | const STATUS_FILTER: StatusBitField = LANDING_GEAR_DEPLOYED 25 | | CARGO_SCOOP_DEPLOYED 26 | | EXTERNAL_LIGHTS_ON 27 | | FRAME_SHIFT_DRIVE_CHARGING 28 | | MASS_LOCKED 29 | | FRAME_SHIFT_DRIVE_COOLDOWN 30 | | OVERHEATING 31 | | SILENT_RUNNING 32 | | HARDPOINTS_DEPLOYED 33 | | SUPERCRUISE 34 | | SPEEDING 35 | | NIGHT_VISION_ON; 36 | 37 | /// An attribute of a `Ship` that can be associated with a value. 38 | #[derive(Clone, Copy, PartialEq)] 39 | pub enum Attribute { 40 | Boost, 41 | CargoScoop, 42 | ExternalLights, 43 | FrameShiftDrive, 44 | Hardpoints, 45 | HeatSink, 46 | LandingGear, 47 | NightVision, 48 | SilentRunning, 49 | Throttle, 50 | } 51 | 52 | /// An association of a `Attribute` to a `StatusLevel` value for a `Ship`. 53 | pub struct Status { 54 | pub attribute: Attribute, 55 | pub level: StatusLevel, 56 | } 57 | 58 | /// A status value that can associated to an `Attibute` through a `Status` 59 | #[derive(Clone, Copy, Debug, PartialEq, PartialOrd)] 60 | pub enum StatusLevel { 61 | Inactive, 62 | Active, 63 | Blocked, 64 | Alert, 65 | } 66 | 67 | /// A condition that can be used to specify a `StatusLevel` through a 68 | /// `ConditionStatusLevelMapping`. 69 | enum Condition { 70 | Any(StatusBitField), 71 | All(StatusBitField), 72 | } 73 | 74 | /// A mapping that defines the `Condition` that indicates a `StatusLevel` 75 | /// applies. 76 | struct ConditionStatusLevelMapping { 77 | condition: Condition, 78 | status_level: StatusLevel, 79 | } 80 | 81 | impl ConditionStatusLevelMapping { 82 | fn new(condition: Condition, status_level: StatusLevel) -> Self { 83 | Self { 84 | condition, 85 | status_level, 86 | } 87 | } 88 | } 89 | 90 | /// A list of `ConditionStatusLevelMapping` instances that apply to an 91 | /// `Attribute`. 92 | struct AttributeStatusLevelMappings { 93 | attribute: Attribute, 94 | condition_status_level_mappings: Vec, 95 | } 96 | 97 | impl AttributeStatusLevelMappings { 98 | fn new( 99 | attribute: Attribute, 100 | condition_status_level_mappings: Vec, 101 | ) -> Self { 102 | Self { 103 | attribute, 104 | condition_status_level_mappings, 105 | } 106 | } 107 | } 108 | 109 | #[derive(Debug, PartialEq)] 110 | pub enum GlobalStatus { 111 | Normal, 112 | HardpointsDeployed, 113 | NightVisionOn, 114 | } 115 | 116 | pub struct Ship { 117 | status_flags: StatusBitField, 118 | attribute_status_level_mappings: Vec, 119 | } 120 | 121 | impl Ship { 122 | /// Returns a `Ship` instance. 123 | pub fn new() -> Self { 124 | Self { 125 | status_flags: 0, 126 | attribute_status_level_mappings: vec![ 127 | AttributeStatusLevelMappings::new( 128 | Attribute::CargoScoop, 129 | vec![ConditionStatusLevelMapping::new( 130 | Condition::All(CARGO_SCOOP_DEPLOYED), 131 | StatusLevel::Active, 132 | )], 133 | ), 134 | AttributeStatusLevelMappings::new( 135 | Attribute::ExternalLights, 136 | vec![ConditionStatusLevelMapping::new( 137 | Condition::All(EXTERNAL_LIGHTS_ON), 138 | StatusLevel::Active, 139 | )], 140 | ), 141 | AttributeStatusLevelMappings::new( 142 | Attribute::FrameShiftDrive, 143 | vec![ 144 | ConditionStatusLevelMapping::new( 145 | Condition::All(FRAME_SHIFT_DRIVE_CHARGING | OVERHEATING), 146 | StatusLevel::Alert, 147 | ), 148 | // Supercruise is higher precendence than normal 149 | // flight, specifically for blocking states like 150 | // hardpoints deployed. 151 | ConditionStatusLevelMapping::new( 152 | Condition::All(SUPERCRUISE | HARDPOINTS_DEPLOYED), 153 | StatusLevel::Inactive, 154 | ), 155 | ConditionStatusLevelMapping::new( 156 | Condition::Any( 157 | CARGO_SCOOP_DEPLOYED 158 | | MASS_LOCKED 159 | | FRAME_SHIFT_DRIVE_COOLDOWN 160 | | HARDPOINTS_DEPLOYED 161 | | LANDING_GEAR_DEPLOYED, 162 | ), 163 | StatusLevel::Blocked, 164 | ), 165 | ConditionStatusLevelMapping::new( 166 | Condition::All(FRAME_SHIFT_DRIVE_CHARGING), 167 | StatusLevel::Active, 168 | ), 169 | ], 170 | ), 171 | AttributeStatusLevelMappings::new( 172 | Attribute::LandingGear, 173 | vec![ 174 | ConditionStatusLevelMapping::new( 175 | Condition::All(LANDING_GEAR_DEPLOYED), 176 | StatusLevel::Active, 177 | ), 178 | ConditionStatusLevelMapping::new( 179 | Condition::All(DOCKING), 180 | StatusLevel::Alert, 181 | ), 182 | ], 183 | ), 184 | AttributeStatusLevelMappings::new( 185 | Attribute::HeatSink, 186 | vec![ConditionStatusLevelMapping::new( 187 | Condition::All(OVERHEATING), 188 | StatusLevel::Alert, 189 | )], 190 | ), 191 | AttributeStatusLevelMappings::new( 192 | Attribute::SilentRunning, 193 | vec![ 194 | ConditionStatusLevelMapping::new( 195 | Condition::All(SILENT_RUNNING | OVERHEATING), 196 | StatusLevel::Alert, 197 | ), 198 | ConditionStatusLevelMapping::new( 199 | Condition::All(SILENT_RUNNING), 200 | StatusLevel::Active, 201 | ), 202 | ], 203 | ), 204 | AttributeStatusLevelMappings::new( 205 | Attribute::Hardpoints, 206 | vec![ConditionStatusLevelMapping::new( 207 | Condition::All(HARDPOINTS_DEPLOYED), 208 | StatusLevel::Active, 209 | )], 210 | ), 211 | AttributeStatusLevelMappings::new( 212 | Attribute::Boost, 213 | vec![ConditionStatusLevelMapping::new( 214 | Condition::All(LANDING_GEAR_DEPLOYED), 215 | StatusLevel::Blocked, 216 | )], 217 | ), 218 | AttributeStatusLevelMappings::new( 219 | Attribute::Throttle, 220 | vec![ConditionStatusLevelMapping::new( 221 | Condition::All(SPEEDING), 222 | StatusLevel::Alert, 223 | )], 224 | ), 225 | AttributeStatusLevelMappings::new( 226 | Attribute::NightVision, 227 | vec![ConditionStatusLevelMapping::new( 228 | Condition::All(NIGHT_VISION_ON), 229 | StatusLevel::Active, 230 | )], 231 | ), 232 | ], 233 | } 234 | } 235 | 236 | /// Updates the ship statuses give the event. 237 | pub fn apply_journal_event(&mut self, event: Event) { 238 | match event { 239 | Event::Docked | Event::DockingCancelled | Event::DockingTimeout => { 240 | info!("Docking terminated"); 241 | self.status_flags &= !DOCKING 242 | } 243 | Event::DockingGranted => { 244 | info!("Docking commenced"); 245 | self.status_flags |= DOCKING 246 | } 247 | Event::Other => warn!("Can't apply `Event::Other` journal event"), 248 | }; 249 | } 250 | 251 | pub fn update_status(&mut self, status: FileStatus) -> bool { 252 | // Flatten non-flag statuses into the bit-field. 253 | let incoming_status_flags = status.flags as u64 254 | | if status.legal_state == LegalState::Speeding { 255 | SPEEDING 256 | } else { 257 | 0 258 | }; 259 | 260 | let updated_status_flags = Self::filtered_status_flags(incoming_status_flags); 261 | 262 | if Self::filtered_status_flags(self.status_flags) == updated_status_flags { 263 | false 264 | } else { 265 | // Reinstate derived status flags that were filtered out (and 266 | // necessarily can't have triggered a status change). 267 | self.status_flags = updated_status_flags | (self.status_flags & DOCKING); 268 | true 269 | } 270 | } 271 | 272 | #[cfg(test)] 273 | // Could refactor this into a private constructor instead. 274 | fn set_status(&mut self, status_flags: StatusBitField) { 275 | self.status_flags = status_flags; 276 | } 277 | 278 | /// Returns a vector of `Status` instances, one each for every `Attribute`, 279 | /// specifying the current `StatusLevel` for that attribute. 280 | pub fn statuses(&self) -> Vec { 281 | let mut statuses = Vec::new(); 282 | 283 | // This should probably be done by functionally mapping the vector. 284 | for mapping in &self.attribute_status_level_mappings { 285 | statuses.push(Status { 286 | attribute: mapping.attribute, 287 | level: self.status_level_for_condition(&mapping.condition_status_level_mappings), 288 | }); 289 | } 290 | 291 | statuses 292 | } 293 | 294 | /// Returns the current global (highest precendence) status for the ship. 295 | pub fn global_status(&self) -> GlobalStatus { 296 | if self.any_status_flags_set(NIGHT_VISION_ON) { 297 | GlobalStatus::NightVisionOn 298 | } else if self.any_status_flags_set(SUPERCRUISE) { 299 | // FSS scans while in supercruise can cause the hardpoint deployed 300 | // status to be set but we don't want this to be the global status. 301 | GlobalStatus::Normal 302 | } else if self.any_status_flags_set(HARDPOINTS_DEPLOYED) { 303 | GlobalStatus::HardpointsDeployed 304 | } else { 305 | GlobalStatus::Normal 306 | } 307 | } 308 | 309 | fn status_level_for_condition( 310 | &self, 311 | mappings: &Vec, 312 | ) -> StatusLevel { 313 | for mapping in mappings { 314 | if match mapping.condition { 315 | Condition::Any(flags) => self.any_status_flags_set(flags), 316 | Condition::All(flags) => self.all_status_flags_set(flags), 317 | } { 318 | return mapping.status_level; 319 | } 320 | } 321 | 322 | StatusLevel::Inactive 323 | } 324 | 325 | fn any_status_flags_set(&self, flags: StatusBitField) -> bool { 326 | self.status_flags & flags != 0 327 | } 328 | 329 | fn all_status_flags_set(&self, flags: StatusBitField) -> bool { 330 | self.status_flags & flags == flags 331 | } 332 | 333 | fn filtered_status_flags(flags: StatusBitField) -> StatusBitField { 334 | flags & STATUS_FILTER 335 | } 336 | } 337 | 338 | #[cfg(test)] 339 | mod tests { 340 | use super::*; 341 | 342 | fn statuses() -> Vec { 343 | vec![ 344 | LANDING_GEAR_DEPLOYED, 345 | EXTERNAL_LIGHTS_ON, 346 | CARGO_SCOOP_DEPLOYED, 347 | SILENT_RUNNING, 348 | FRAME_SHIFT_DRIVE_CHARGING, 349 | MASS_LOCKED, 350 | FRAME_SHIFT_DRIVE_COOLDOWN, 351 | OVERHEATING, 352 | HARDPOINTS_DEPLOYED, 353 | SUPERCRUISE, 354 | NIGHT_VISION_ON, 355 | ] 356 | } 357 | 358 | #[test] 359 | fn ship_update_status_sets_statuses() { 360 | for flag in statuses() { 361 | let mut ship = Ship::new(); 362 | ship.update_status(FileStatus { 363 | flags: flag as u32, 364 | legal_state: LegalState::Other, 365 | }); 366 | assert_eq!(ship.all_status_flags_set(flag), true); 367 | } 368 | } 369 | 370 | #[test] 371 | fn ship_update_status_clears_statuses() { 372 | for flag in statuses() { 373 | let mut ship = Ship::new(); 374 | ship.set_status(flag); 375 | ship.update_status(FileStatus { 376 | flags: 0, 377 | legal_state: LegalState::Other, 378 | }); 379 | assert_eq!(ship.all_status_flags_set(flag), false); 380 | } 381 | } 382 | 383 | #[test] 384 | fn ship_update_status_returns_true_on_change() { 385 | for flag in statuses() { 386 | let mut ship = Ship::new(); 387 | assert_eq!( 388 | ship.update_status(FileStatus { 389 | flags: flag as u32, 390 | legal_state: LegalState::Other, 391 | }), 392 | true 393 | ); 394 | assert_eq!( 395 | ship.update_status(FileStatus { 396 | flags: flag as u32, 397 | legal_state: LegalState::Other, 398 | }), 399 | false 400 | ); 401 | } 402 | } 403 | 404 | #[test] 405 | fn ship_update_status_does_not_clobber_derived_states() { 406 | let mut ship = Ship::new(); 407 | ship.set_status(DOCKING); 408 | ship.update_status(FileStatus { 409 | flags: LANDING_GEAR_DEPLOYED as u32, 410 | legal_state: LegalState::Other, 411 | }); 412 | assert_eq!(ship.all_status_flags_set(DOCKING), true); 413 | } 414 | 415 | fn assert_status(status_flags: StatusBitField, attribute: Attribute, level: StatusLevel) { 416 | let mut ship = Ship::new(); 417 | ship.set_status(status_flags); 418 | let statuses = ship.statuses(); 419 | let status = statuses 420 | .iter() 421 | .find(|&status| status.attribute == attribute) 422 | .expect("Statuses did not include expected attribute"); 423 | 424 | assert_eq!(status.level, level); 425 | } 426 | 427 | #[test] 428 | fn ship_update_status_sets_speeding() { 429 | let mut ship = Ship::new(); 430 | ship.update_status(FileStatus { 431 | flags: 0, 432 | legal_state: LegalState::Speeding, 433 | }); 434 | assert_eq!(ship.all_status_flags_set(SPEEDING), true); 435 | } 436 | 437 | #[test] 438 | fn ship_update_status_clears_speeding() { 439 | let mut ship = Ship::new(); 440 | ship.update_status(FileStatus { 441 | flags: 0, 442 | legal_state: LegalState::Other, 443 | }); 444 | assert_eq!(ship.all_status_flags_set(SPEEDING), false); 445 | } 446 | 447 | #[test] 448 | fn ship_update_speeding_returns_true_on_change() { 449 | let mut ship = Ship::new(); 450 | assert_eq!( 451 | ship.update_status(FileStatus { 452 | flags: 0, 453 | legal_state: LegalState::Speeding, 454 | }), 455 | true 456 | ); 457 | assert_eq!( 458 | ship.update_status(FileStatus { 459 | flags: 0, 460 | legal_state: LegalState::Speeding, 461 | }), 462 | false 463 | ); 464 | } 465 | 466 | #[test] 467 | fn ship_update_status_speeding_does_not_clobber_derived_states() { 468 | let mut ship = Ship::new(); 469 | ship.set_status(DOCKING); 470 | ship.update_status(FileStatus { 471 | flags: 0, 472 | legal_state: LegalState::Speeding, 473 | }); 474 | assert_eq!(ship.all_status_flags_set(DOCKING), true); 475 | } 476 | 477 | #[test] 478 | fn zero_state_maps_to_cargo_scoop_inactive() { 479 | assert_status(0, Attribute::CargoScoop, StatusLevel::Inactive); 480 | } 481 | 482 | #[test] 483 | fn zero_state_maps_to_external_lights_inactive() { 484 | assert_status(0, Attribute::ExternalLights, StatusLevel::Inactive); 485 | } 486 | 487 | #[test] 488 | fn zero_state_maps_to_frame_shift_drive_inactive() { 489 | assert_status(0, Attribute::FrameShiftDrive, StatusLevel::Inactive); 490 | } 491 | 492 | #[test] 493 | fn zero_state_maps_to_landing_gear_inactive() { 494 | assert_status(0, Attribute::LandingGear, StatusLevel::Inactive); 495 | } 496 | 497 | #[test] 498 | fn zero_state_maps_to_night_vision_inactive() { 499 | assert_status(0, Attribute::NightVision, StatusLevel::Inactive); 500 | } 501 | 502 | #[test] 503 | fn zero_state_maps_to_silent_running_inactive() { 504 | assert_status(0, Attribute::SilentRunning, StatusLevel::Inactive); 505 | } 506 | 507 | #[test] 508 | fn zero_state_maps_to_throttle_inactive() { 509 | assert_status(0, Attribute::Throttle, StatusLevel::Inactive); 510 | } 511 | 512 | #[test] 513 | fn cargo_scoop_deployed_maps_to_cargo_scoop_active() { 514 | assert_status( 515 | CARGO_SCOOP_DEPLOYED, 516 | Attribute::CargoScoop, 517 | StatusLevel::Active, 518 | ); 519 | } 520 | 521 | #[test] 522 | fn cargo_scoop_deployed_maps_to_frame_shift_drive_blocked() { 523 | assert_status( 524 | CARGO_SCOOP_DEPLOYED, 525 | Attribute::FrameShiftDrive, 526 | StatusLevel::Blocked, 527 | ); 528 | } 529 | 530 | #[test] 531 | fn external_lights_on_maps_to_external_lights_active() { 532 | assert_status( 533 | EXTERNAL_LIGHTS_ON, 534 | Attribute::ExternalLights, 535 | StatusLevel::Active, 536 | ); 537 | } 538 | 539 | #[test] 540 | fn frame_shift_drive_charging_maps_to_frame_shift_drive_active() { 541 | assert_status( 542 | FRAME_SHIFT_DRIVE_CHARGING, 543 | Attribute::FrameShiftDrive, 544 | StatusLevel::Active, 545 | ); 546 | } 547 | 548 | #[test] 549 | fn frame_shift_drive_charging_and_overheating_maps_to_frame_shift_drive_alert() { 550 | assert_status( 551 | FRAME_SHIFT_DRIVE_CHARGING + OVERHEATING, 552 | Attribute::FrameShiftDrive, 553 | StatusLevel::Alert, 554 | ); 555 | } 556 | 557 | #[test] 558 | fn frame_shift_drive_charging_and_supercruise_and_overheating_maps_to_frame_shift_drive_alert() 559 | { 560 | assert_status( 561 | FRAME_SHIFT_DRIVE_CHARGING + SUPERCRUISE + OVERHEATING, 562 | Attribute::FrameShiftDrive, 563 | StatusLevel::Alert, 564 | ); 565 | } 566 | 567 | #[test] 568 | fn frame_shift_drive_cooldown_maps_to_frame_shift_drive_blocked() { 569 | assert_status( 570 | FRAME_SHIFT_DRIVE_COOLDOWN, 571 | Attribute::FrameShiftDrive, 572 | StatusLevel::Blocked, 573 | ); 574 | } 575 | 576 | #[test] 577 | fn hardpoints_deployed_maps_to_hardpoints_active() { 578 | assert_status( 579 | HARDPOINTS_DEPLOYED, 580 | Attribute::Hardpoints, 581 | StatusLevel::Active, 582 | ); 583 | } 584 | 585 | #[test] 586 | fn hardpoints_deployed_maps_to_frame_shift_drive_blocked() { 587 | assert_status( 588 | HARDPOINTS_DEPLOYED, 589 | Attribute::FrameShiftDrive, 590 | StatusLevel::Blocked, 591 | ); 592 | } 593 | 594 | #[test] 595 | fn landing_gear_deployed_maps_to_landing_gear_active() { 596 | assert_status( 597 | LANDING_GEAR_DEPLOYED, 598 | Attribute::LandingGear, 599 | StatusLevel::Active, 600 | ); 601 | } 602 | 603 | #[test] 604 | fn landing_gear_not_deployed_and_docking_maps_to_landing_gear_alert() { 605 | assert_status(DOCKING, Attribute::LandingGear, StatusLevel::Alert); 606 | } 607 | 608 | #[test] 609 | fn landing_gear_deployed_maps_to_boost_blocked() { 610 | assert_status( 611 | LANDING_GEAR_DEPLOYED, 612 | Attribute::Boost, 613 | StatusLevel::Blocked, 614 | ); 615 | } 616 | 617 | #[test] 618 | fn landing_gear_deployed_maps_to_frame_shift_drive_blocked() { 619 | assert_status( 620 | LANDING_GEAR_DEPLOYED, 621 | Attribute::FrameShiftDrive, 622 | StatusLevel::Blocked, 623 | ); 624 | } 625 | 626 | #[test] 627 | fn mass_locked_maps_to_frame_shift_drive_blocked() { 628 | assert_status( 629 | MASS_LOCKED, 630 | Attribute::FrameShiftDrive, 631 | StatusLevel::Blocked, 632 | ); 633 | } 634 | 635 | #[test] 636 | fn night_vision_on_maps_to_night_vision_active() { 637 | assert_status(NIGHT_VISION_ON, Attribute::NightVision, StatusLevel::Active); 638 | } 639 | 640 | #[test] 641 | fn overheating_maps_to_heat_sink_alert() { 642 | assert_status(OVERHEATING, Attribute::HeatSink, StatusLevel::Alert); 643 | } 644 | #[test] 645 | fn silent_running_maps_to_silent_running_active() { 646 | assert_status( 647 | SILENT_RUNNING, 648 | Attribute::SilentRunning, 649 | StatusLevel::Active, 650 | ); 651 | } 652 | 653 | #[test] 654 | fn silent_running_and_overheating_maps_to_silent_running_alert() { 655 | assert_status( 656 | SILENT_RUNNING + OVERHEATING, 657 | Attribute::SilentRunning, 658 | StatusLevel::Alert, 659 | ); 660 | } 661 | 662 | #[test] 663 | fn speeding_maps_to_throttle_alert() { 664 | assert_status(SPEEDING, Attribute::Throttle, StatusLevel::Alert); 665 | } 666 | 667 | #[test] 668 | fn supercruise_and_hardpoints_deployed_and_maps_to_frame_shift_drive_inactive() { 669 | assert_status( 670 | SUPERCRUISE + HARDPOINTS_DEPLOYED, 671 | Attribute::FrameShiftDrive, 672 | StatusLevel::Inactive, 673 | ); 674 | } 675 | 676 | fn assert_global_status(status_flags: StatusBitField, expected_global_status: GlobalStatus) { 677 | let mut ship = Ship::new(); 678 | ship.set_status(status_flags); 679 | assert_eq!(ship.global_status(), expected_global_status); 680 | } 681 | 682 | #[test] 683 | fn global_status_precedence_rules() { 684 | assert_global_status(0, GlobalStatus::Normal); 685 | assert_global_status(HARDPOINTS_DEPLOYED, GlobalStatus::HardpointsDeployed); 686 | assert_global_status(HARDPOINTS_DEPLOYED | SUPERCRUISE, GlobalStatus::Normal); 687 | assert_global_status(NIGHT_VISION_ON, GlobalStatus::NightVisionOn); 688 | } 689 | } 690 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod config; 2 | mod events; 3 | mod game; 4 | mod x52pro; 5 | 6 | use config::Config; 7 | use events::Event; 8 | use game::{file::journal, file::journal::JournalReader, file::Status}; 9 | use game::{Attribute, Control, Controls, Ship}; 10 | use hotwatch::Hotwatch; 11 | use log::{debug, info}; 12 | use std::sync::mpsc; 13 | use std::thread; 14 | use std::time::Duration; 15 | use x52pro::{Device, StatusLevelToModeMapper}; 16 | 17 | const ANIMATION_TICK_MILLISECONDS: u64 = x52pro::ALERT_FLASH_MILLISECONDS as u64; 18 | 19 | pub fn run(config: Config) { 20 | let mut x52pro = Device::new(); 21 | 22 | let bindings_file_path = config.bindings_file_path(); 23 | debug!("Bindings file path: {:?}", bindings_file_path); 24 | 25 | let controls = Controls::from_file(&bindings_file_path); 26 | debug!("Controls: {:?}", controls); 27 | 28 | let status_file_path = game::file::status_file_path(); 29 | debug!("Status file path: {:?}", status_file_path); 30 | 31 | let mut ship = Ship::new(); 32 | let (tx, rx) = mpsc::channel(); 33 | 34 | let mut journal_reader = JournalReader::new(); 35 | 36 | // Need to send this event before status so journal is read too. 37 | if let Some(journal_file_path) = game::file::latest_journal_file_path() { 38 | tx.send(Event::NewJournalFile(journal_file_path)) 39 | .expect("Can't send new journal file message for latest journal file"); 40 | } else { 41 | debug!("No latest journal file found"); 42 | } 43 | 44 | let initial_status = 45 | Status::from_file(&status_file_path).expect("Could not read current status"); 46 | tx.send(Event::StatusUpdate(initial_status)) 47 | .expect("Could not send status update message"); 48 | 49 | let tx2 = tx.clone(); 50 | let tx3 = tx.clone(); 51 | let mut hotwatch = Hotwatch::new_with_custom_delay(Duration::from_millis(100)) 52 | .expect("File watcher failed to initialize"); 53 | 54 | // Could pass a closure here to decouple the function from the event 55 | // raising, although we'd be back to cloning `tx` locally. 56 | journal::watch_dir(game::file::journal_dir_path(), &mut hotwatch, &tx); 57 | 58 | hotwatch 59 | .watch(status_file_path, move |event: hotwatch::Event| { 60 | if let hotwatch::Event::Write(path) = event { 61 | if let Some(status) = Status::from_file(&path) { 62 | tx.send(Event::StatusUpdate(status)) 63 | .expect("Could not send status update message"); 64 | } 65 | } 66 | }) 67 | .expect("Failed to watch status file"); 68 | 69 | info!("Press Ctrl+C to exit"); 70 | ctrlc::set_handler(move || { 71 | info!("Received Ctrl+C"); 72 | tx2.send(Event::Exit).expect("Could not send exit message"); 73 | }) 74 | .expect("Failed to set Ctrl+C handler"); 75 | 76 | thread::spawn(move || loop { 77 | thread::sleep(Duration::from_millis(ANIMATION_TICK_MILLISECONDS)); 78 | tx3.send(Event::AnimationTick) 79 | .expect("Could not send animation tick message"); 80 | }); 81 | 82 | for event in rx { 83 | match event { 84 | Event::NewJournalFile(file_path) => journal_reader.open(file_path), 85 | Event::Exit => break, 86 | Event::AnimationTick => x52pro.update_animated_lights(), 87 | Event::StatusUpdate(status) => { 88 | // Unlike the status file, it appears that the current journal 89 | // file is kept open by the game, which in turn appears to 90 | // prevent write events being raised immediately on the file, 91 | // meaning we can't watch it for changes. Instead, we try 92 | // reading each time the status file is re-written. 93 | let journal_events = journal_reader.new_events(); 94 | let journal_events_present = !journal_events.is_empty(); 95 | 96 | for journal_event in journal_events { 97 | ship.apply_journal_event(journal_event); 98 | } 99 | 100 | // Could push the new journal events into `update_status` or 101 | // even pass in the reader itself, although that's increasing 102 | // the coupling. 103 | if ship.update_status(status) | journal_events_present { 104 | set_x52pro_inputs_from_ship_statues( 105 | &mut x52pro, 106 | &controls, 107 | ship.statuses(), 108 | &config.status_level_to_mode_mapper(ship.global_status()), 109 | ); 110 | } else { 111 | debug!("Status file updated but change not relevant"); 112 | } 113 | } 114 | } 115 | } 116 | 117 | info!("Exiting"); 118 | } 119 | 120 | fn set_x52pro_inputs_from_ship_statues( 121 | x52pro: &mut Device, 122 | controls: &Controls, 123 | statuses: Vec, 124 | status_level_to_mode_mapper: &StatusLevelToModeMapper, 125 | ) { 126 | fn controls_for_status(status: &game::Status) -> Vec { 127 | match status.attribute { 128 | Attribute::Boost => vec![Control::Boost], 129 | Attribute::CargoScoop => vec![Control::CargoScoop], 130 | Attribute::ExternalLights => vec![Control::ExternalLights], 131 | Attribute::FrameShiftDrive => vec![ 132 | Control::Hyperspace, 133 | Control::HyperSuperCombination, 134 | Control::Supercruise, 135 | ], 136 | Attribute::Hardpoints => vec![Control::Hardpoints], 137 | Attribute::HeatSink => vec![Control::HeatSink], 138 | Attribute::LandingGear => vec![Control::LandingGear], 139 | Attribute::NightVision => vec![Control::NightVision], 140 | Attribute::SilentRunning => vec![Control::SilentRunning], 141 | Attribute::Throttle => vec![Control::Throttle], 142 | } 143 | } 144 | 145 | let mut input_status_levels = Vec::new(); 146 | 147 | // This can probably be written functionally by mapping. 148 | for status in statuses { 149 | for control in controls_for_status(&status) { 150 | for input in controls.inputs_for_control(control) { 151 | debug!("Input={:?}, StatusLevel={:?}", input, status.level); 152 | input_status_levels.push((input, status.level.clone())); 153 | } 154 | } 155 | } 156 | 157 | x52pro.set_input_status_levels(input_status_levels, status_level_to_mode_mapper); 158 | } 159 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use edxlc::config::Config; 2 | use log::{debug, info}; 3 | use std::env; 4 | 5 | const CONFIG_FILENAME: &str = "edxlc.toml"; 6 | 7 | #[cfg(debug_assertions)] 8 | const DEFAULT_LOG_LEVEL: &str = "edxlc=debug"; 9 | #[cfg(not(debug_assertions))] 10 | const DEFAULT_LOG_LEVEL: &str = "info"; 11 | 12 | const VERSION: &str = env!("CARGO_PKG_VERSION"); 13 | 14 | fn main() { 15 | configure_logger(); 16 | info!("EDXLC {}", VERSION); 17 | 18 | edxlc::config::write_default_file_if_missing(CONFIG_FILENAME); 19 | let config = Config::from_file(config_filename()); 20 | debug!("{:?}", config); 21 | 22 | edxlc::run(config); 23 | } 24 | 25 | fn config_filename() -> String { 26 | let args: Vec = env::args().collect(); 27 | 28 | if args.len() < 2 { 29 | let config_filename = String::from(CONFIG_FILENAME); 30 | debug!("Using default configuration filename: {}", config_filename); 31 | config_filename 32 | } else { 33 | let config_filename = &args[1]; 34 | debug!( 35 | "Using command line configuration filename: {}", 36 | config_filename 37 | ); 38 | config_filename.clone() 39 | } 40 | } 41 | 42 | fn configure_logger() { 43 | env_logger::Builder::from_env(env_logger::Env::default().default_filter_or(DEFAULT_LOG_LEVEL)) 44 | .init(); 45 | } 46 | -------------------------------------------------------------------------------- /src/resources/edxlc.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewdsmith/edxlc/aa277088bf969b00ba1bfd57b4d448da34787538/src/resources/edxlc.ico -------------------------------------------------------------------------------- /src/resources/edxlc.rc: -------------------------------------------------------------------------------- 1 | 1 ICON "edxlc.ico" -------------------------------------------------------------------------------- /src/x52pro.rs: -------------------------------------------------------------------------------- 1 | pub mod device; 2 | pub mod direct_output; 3 | mod light_mode_to_state_mapper; 4 | mod status_level_to_mode_mapper; 5 | 6 | pub use device::Device; 7 | pub use light_mode_to_state_mapper::{LightModeToStateMapper, ALERT_FLASH_MILLISECONDS}; 8 | pub use status_level_to_mode_mapper::StatusLevelToModeMapper; 9 | -------------------------------------------------------------------------------- /src/x52pro/device.rs: -------------------------------------------------------------------------------- 1 | use crate::game::StatusLevel; 2 | use crate::x52pro::{direct_output::DirectOutput, LightModeToStateMapper, StatusLevelToModeMapper}; 3 | use enum_iterator::IntoEnumIterator; 4 | use serde::{Deserialize, Serialize}; 5 | use std::collections::HashMap; 6 | 7 | /// Controllable LEDs on the device. Assigned values correspond to the ids used 8 | /// by DirectOutput. 9 | #[derive(Copy, Clone)] 10 | pub enum Led { 11 | Fire = 0, 12 | FireARed = 1, 13 | FireAGreen = 2, 14 | FireBRed = 3, 15 | FireBGreen = 4, 16 | FireDRed = 5, 17 | FireDGreen = 6, 18 | FireERed = 7, 19 | FireEGreen = 8, 20 | T1T2Red = 9, 21 | T1T2Green = 10, 22 | T3T4Red = 11, 23 | T3T4Green = 12, 24 | T5T6Red = 13, 25 | T5T6Green = 14, 26 | PoV2Red = 15, 27 | PoV2Green = 16, 28 | ClutchRed = 17, 29 | ClutchGreen = 18, 30 | Throttle = 19, 31 | } 32 | 33 | /// An instance of an interface to a Saitek X52 Pro Flight HOTAS flight 34 | /// controller device. 35 | pub struct Device { 36 | direct_output: DirectOutput, 37 | lights: HashMap>, 38 | animated_lights: Vec, 39 | light_mode_to_state_mapper: LightModeToStateMapper, 40 | } 41 | 42 | impl Device { 43 | /// Returns a new instance of the device interface. Panics if the 44 | /// underlying `DirectOutput` instance cannot be loaded. 45 | pub fn new() -> Self { 46 | let mut direct_output = DirectOutput::load(); 47 | direct_output.initialize(); 48 | direct_output.enumerate(); 49 | direct_output.add_page(); 50 | 51 | let mut lights = HashMap::>::new(); 52 | 53 | lights.insert( 54 | Light::Clutch, 55 | Box::new(RedGreenLightMapping::new(Led::ClutchRed, Led::ClutchGreen)), 56 | ); 57 | lights.insert(Light::Fire, Box::new(BinaryLightMapping::new(Led::Fire))); 58 | lights.insert( 59 | Light::FireA, 60 | Box::new(RedGreenLightMapping::new(Led::FireARed, Led::FireAGreen)), 61 | ); 62 | lights.insert( 63 | Light::FireB, 64 | Box::new(RedGreenLightMapping::new(Led::FireBRed, Led::FireBGreen)), 65 | ); 66 | lights.insert( 67 | Light::FireD, 68 | Box::new(RedGreenLightMapping::new(Led::FireDRed, Led::FireDGreen)), 69 | ); 70 | lights.insert( 71 | Light::FireE, 72 | Box::new(RedGreenLightMapping::new(Led::FireERed, Led::FireEGreen)), 73 | ); 74 | lights.insert( 75 | Light::PoV2, 76 | Box::new(RedGreenLightMapping::new(Led::PoV2Red, Led::PoV2Green)), 77 | ); 78 | lights.insert( 79 | Light::T1T2, 80 | Box::new(RedGreenLightMapping::new(Led::T1T2Red, Led::T1T2Green)), 81 | ); 82 | lights.insert( 83 | Light::T3T4, 84 | Box::new(RedGreenLightMapping::new(Led::T3T4Red, Led::T3T4Green)), 85 | ); 86 | lights.insert( 87 | Light::T5T6, 88 | Box::new(RedGreenLightMapping::new(Led::T5T6Red, Led::T5T6Green)), 89 | ); 90 | lights.insert( 91 | Light::Throttle, 92 | Box::new(BinaryLightMapping::new(Led::Throttle)), 93 | ); 94 | 95 | Device { 96 | direct_output, 97 | lights, 98 | animated_lights: vec![], 99 | light_mode_to_state_mapper: LightModeToStateMapper::new(), 100 | } 101 | } 102 | 103 | /// Sets each input to specified status level. Repeated inputs with 104 | /// different status levels are handled by using the highest value. The Light 105 | /// for the input is looked up, as is the Light state for the status level. 106 | pub fn set_input_status_levels( 107 | &mut self, 108 | input_status_levels: Vec<(Input, StatusLevel)>, 109 | status_level_to_mode_mapper: &StatusLevelToModeMapper, 110 | ) { 111 | // A hash mapping every light to the highest status level encountered. 112 | // This creation could be moved into `new`. 113 | let mut light_highest_status_levels = HashMap::new(); 114 | 115 | // Default all lights to inactive. 116 | for light in Light::into_enum_iter() { 117 | light_highest_status_levels.insert(light, StatusLevel::Inactive); 118 | } 119 | 120 | // Map given inputs to corresponding light and update the hash if the 121 | // status level is higher. It should be entirely safe to call `unwrap` 122 | // as we know we have an entry in the hash for every light. 123 | for (input, status_level) in input_status_levels { 124 | let light = light_for_input(input); 125 | let light_status_level = light_highest_status_levels.get_mut(&light).unwrap(); 126 | 127 | if status_level > *light_status_level { 128 | *light_status_level = status_level.clone(); 129 | } 130 | } 131 | 132 | // Update the list of lights that are in a mode that requires animation. 133 | self.animated_lights.clear(); 134 | 135 | for (light, status_level) in &light_highest_status_levels { 136 | let light_mode = status_level_to_mode_mapper.map(status_level); 137 | let light_mapping = self.lights.get_mut(light).expect("Can't find light"); 138 | 139 | light_mapping.set_mode(light_mode); 140 | light_mapping.update_state(&self.direct_output, &self.light_mode_to_state_mapper); 141 | 142 | if light_mapping.is_animated() { 143 | self.animated_lights.push(*light); 144 | } 145 | } 146 | } 147 | 148 | /// Updates lights that have a state that is animated, e.g. flashing. This 149 | /// needs to be called frequently for proper animation. 150 | // 151 | // Ideally the device would manage its own threading for animation but 152 | // this would require state updates to be communicated asynchronously. 153 | pub fn update_animated_lights(&self) { 154 | for light in &self.animated_lights { 155 | let light_mapping = self.lights.get(light).expect("Can't find light"); 156 | light_mapping.update_state(&self.direct_output, &self.light_mode_to_state_mapper); 157 | } 158 | } 159 | } 160 | 161 | /// Supported input buttons or axes on the device. 162 | #[derive(Debug, PartialEq)] 163 | pub enum Input { 164 | Clutch, 165 | Fire, 166 | FireA, 167 | FireB, 168 | FireD, 169 | FireE, 170 | PoV2Down, 171 | PoV2Left, 172 | PoV2Right, 173 | PoV2Up, 174 | T1, 175 | T2, 176 | T3, 177 | T4, 178 | T5, 179 | T6, 180 | ZAxis, 181 | } 182 | 183 | /// Controllable lights on the device, which have either one or two LEDs. 184 | #[derive(Copy, Clone, Debug, Eq, Hash, IntoEnumIterator, PartialEq)] 185 | enum Light { 186 | Clutch, 187 | Fire, 188 | FireA, 189 | FireB, 190 | FireD, 191 | FireE, 192 | PoV2, 193 | T1T2, 194 | T3T4, 195 | T5T6, 196 | Throttle, 197 | } 198 | 199 | /// Available modes for boolean lights on the device. 200 | #[derive(Copy, Clone, Debug, Deserialize, PartialEq, Serialize)] 201 | #[serde(rename_all = "kebab-case")] 202 | pub enum BooleanLightMode { 203 | Off, 204 | On, 205 | Flash, 206 | } 207 | 208 | impl BooleanLightMode { 209 | /// Returns true if the mode requires animation, i.e. changes over time. 210 | fn is_animated(&self) -> bool { 211 | match self { 212 | Self::Flash => true, 213 | _ => false, 214 | } 215 | } 216 | } 217 | 218 | /// Available modes for red/amber/green lights on the device. 219 | #[derive(Copy, Clone, Debug, Deserialize, PartialEq, Serialize)] 220 | #[serde(rename_all = "kebab-case")] 221 | pub enum RedAmberGreenLightMode { 222 | Off, 223 | Red, 224 | Amber, 225 | Green, 226 | // This variant is the same as `RedAmberFlash` but left in for backwards 227 | // compatability with older configuration files. 228 | RedAmber, 229 | RedFlash, 230 | RedAmberFlash, 231 | RedGreenFlash, 232 | AmberFlash, 233 | AmberRedFlash, 234 | AmberGreenFlash, 235 | GreenFlash, 236 | GreenAmberFlash, 237 | GreenRedFlash, 238 | } 239 | 240 | impl RedAmberGreenLightMode { 241 | /// Returns true if the mode requires animation, i.e. changes over time. 242 | fn is_animated(&self) -> bool { 243 | match self { 244 | RedAmberGreenLightMode::Off 245 | | RedAmberGreenLightMode::Red 246 | | RedAmberGreenLightMode::Amber 247 | | RedAmberGreenLightMode::Green => false, 248 | _ => true, 249 | } 250 | } 251 | } 252 | 253 | #[derive(Copy, Clone, Debug, PartialEq)] 254 | pub struct LightMode { 255 | pub boolean: BooleanLightMode, 256 | pub red_amber_green: RedAmberGreenLightMode, 257 | } 258 | 259 | impl LightMode { 260 | pub fn new(boolean: BooleanLightMode, red_amber_green: RedAmberGreenLightMode) -> Self { 261 | Self { 262 | boolean, 263 | red_amber_green, 264 | } 265 | } 266 | } 267 | 268 | /// Common methods for interacting with light mapped to one or more device LEDs. 269 | trait LightMapping { 270 | /// Returns true if the light's currently set mode is animated. 271 | fn is_animated(&self) -> bool; 272 | 273 | /// Updates the light's mode. 274 | fn set_mode(&mut self, light_mode: LightMode); 275 | 276 | /// Updates the mapped LEDs using the given `DirectOutput` object and based 277 | /// on the current mode and the given `LightModeToStateMapper`. 278 | fn update_state( 279 | &self, 280 | direct_output: &DirectOutput, 281 | light_mode_to_state_mapper: &LightModeToStateMapper, 282 | ); 283 | } 284 | 285 | /// The mapping of a light to a single device LED. 286 | struct BinaryLightMapping { 287 | led_id: Led, 288 | light_mode: BooleanLightMode, 289 | } 290 | 291 | impl BinaryLightMapping { 292 | fn new(led_id: Led) -> Self { 293 | Self { 294 | led_id, 295 | light_mode: BooleanLightMode::Off, 296 | } 297 | } 298 | } 299 | 300 | impl LightMapping for BinaryLightMapping { 301 | fn is_animated(&self) -> bool { 302 | self.light_mode.is_animated() 303 | } 304 | 305 | fn set_mode(&mut self, light_mode: LightMode) { 306 | self.light_mode = light_mode.boolean; 307 | } 308 | 309 | fn update_state( 310 | &self, 311 | direct_output: &DirectOutput, 312 | light_mode_to_state_mapper: &LightModeToStateMapper, 313 | ) { 314 | light_mode_to_state_mapper.update_binary_light( 315 | direct_output, 316 | &self.light_mode, 317 | self.led_id, 318 | ); 319 | } 320 | } 321 | 322 | /// The mapping of a light to a red-green pair of device LEDs. 323 | struct RedGreenLightMapping { 324 | red_led_id: Led, 325 | green_led_id: Led, 326 | light_mode: RedAmberGreenLightMode, 327 | } 328 | 329 | impl RedGreenLightMapping { 330 | fn new(red_led_id: Led, green_led_id: Led) -> Self { 331 | Self { 332 | red_led_id, 333 | green_led_id, 334 | light_mode: RedAmberGreenLightMode::Off, 335 | } 336 | } 337 | } 338 | 339 | impl LightMapping for RedGreenLightMapping { 340 | fn is_animated(&self) -> bool { 341 | self.light_mode.is_animated() 342 | } 343 | 344 | fn set_mode(&mut self, light_mode: LightMode) { 345 | self.light_mode = light_mode.red_amber_green; 346 | } 347 | 348 | fn update_state( 349 | &self, 350 | direct_output: &DirectOutput, 351 | light_mode_to_state_mapper: &LightModeToStateMapper, 352 | ) { 353 | light_mode_to_state_mapper.update_red_amber_green_light( 354 | direct_output, 355 | &self.light_mode, 356 | self.red_led_id, 357 | self.green_led_id, 358 | ); 359 | } 360 | } 361 | 362 | /// Returns the Light that corresponds to a given input. Note that in some cases, 363 | /// specifically the T buttons, multiple inputs share an Light. 364 | fn light_for_input(input: Input) -> Light { 365 | match input { 366 | Input::Clutch => Light::Clutch, 367 | Input::Fire => Light::Fire, 368 | Input::FireA => Light::FireA, 369 | Input::FireB => Light::FireB, 370 | Input::FireD => Light::FireD, 371 | Input::FireE => Light::FireE, 372 | Input::PoV2Down => Light::PoV2, 373 | Input::PoV2Left => Light::PoV2, 374 | Input::PoV2Right => Light::PoV2, 375 | Input::PoV2Up => Light::PoV2, 376 | Input::T1 => Light::T1T2, 377 | Input::T2 => Light::T1T2, 378 | Input::T3 => Light::T3T4, 379 | Input::T4 => Light::T3T4, 380 | Input::T5 => Light::T5T6, 381 | Input::T6 => Light::T5T6, 382 | Input::ZAxis => Light::Throttle, 383 | } 384 | } 385 | 386 | #[cfg(test)] 387 | mod tests { 388 | use super::*; 389 | 390 | #[test] 391 | fn input_to_light_permutations() { 392 | assert_light_for_input(Input::Clutch, Light::Clutch); 393 | assert_light_for_input(Input::Fire, Light::Fire); 394 | assert_light_for_input(Input::FireA, Light::FireA); 395 | assert_light_for_input(Input::FireB, Light::FireB); 396 | assert_light_for_input(Input::FireD, Light::FireD); 397 | assert_light_for_input(Input::FireE, Light::FireE); 398 | assert_light_for_input(Input::PoV2Up, Light::PoV2); 399 | assert_light_for_input(Input::PoV2Down, Light::PoV2); 400 | assert_light_for_input(Input::PoV2Left, Light::PoV2); 401 | assert_light_for_input(Input::PoV2Right, Light::PoV2); 402 | assert_light_for_input(Input::T1, Light::T1T2); 403 | assert_light_for_input(Input::T2, Light::T1T2); 404 | assert_light_for_input(Input::T3, Light::T3T4); 405 | assert_light_for_input(Input::T4, Light::T3T4); 406 | assert_light_for_input(Input::T5, Light::T5T6); 407 | assert_light_for_input(Input::T6, Light::T5T6); 408 | assert_light_for_input(Input::ZAxis, Light::Throttle); 409 | } 410 | 411 | fn assert_light_for_input(input: Input, light: Light) { 412 | assert_eq!(light_for_input(input), light); 413 | } 414 | } 415 | -------------------------------------------------------------------------------- /src/x52pro/direct_output.rs: -------------------------------------------------------------------------------- 1 | use libc::c_void; 2 | use libloading::{os::windows::Symbol, Library}; 3 | use log::debug; 4 | use std::ffi::OsStr; 5 | use std::iter::once; 6 | use std::os::windows::ffi::OsStrExt; 7 | use winapi::ctypes::wchar_t; 8 | use winapi::shared::minwindef::DWORD; 9 | use winapi::um::winnt::HRESULT; 10 | use winreg::enums::HKEY_LOCAL_MACHINE; 11 | use winreg::RegKey; 12 | 13 | type DeviceHandle = *const c_void; 14 | 15 | type InitializeFn = unsafe extern "C" fn(wszPluginName: *const wchar_t) -> HRESULT; 16 | type EnumerateFn = 17 | unsafe extern "C" fn(pfnCb: EnumerateCallbackFn, pCtxt: &mut DirectOutput) -> HRESULT; 18 | type EnumerateCallbackFn = extern "C" fn(hDevice: DeviceHandle, pCtxt: &mut DirectOutput); 19 | type AddPageFn = unsafe extern "C" fn( 20 | hDevice: DeviceHandle, 21 | dwPage: DWORD, 22 | wszDebugName: *const wchar_t, 23 | dwFlags: DWORD, 24 | ) -> HRESULT; 25 | type SetLedFn = unsafe extern "C" fn( 26 | hDevice: DeviceHandle, 27 | dwPage: DWORD, 28 | dwIndex: DWORD, 29 | dwValue: DWORD, 30 | ) -> HRESULT; 31 | 32 | const FLAG_SET_AS_ACTIVE: DWORD = 1; 33 | 34 | const PLUGIN_NAME: &str = "EDXLC"; 35 | const PAGE_ID: DWORD = 1; 36 | 37 | const REGISTRY_KEY_NAME: &str = r"DirectOutput"; 38 | const REGISTRY_KEY_PATH: &str = r"SOFTWARE\Logitech\DirectOutput"; 39 | 40 | /// An instance of a safe wrapper around the Saitek DirectOutput library. 41 | pub struct DirectOutput { 42 | // We have to continue to own the Library instance even though we never use 43 | // it again so that it is not dropped and hence closed, which would 44 | // invalidate the symbols loaded from it we want to use to call functions. 45 | #[allow(dead_code)] 46 | library: Library, 47 | initialize_fn: Symbol, 48 | enumerate_fn: Symbol, 49 | add_page_fn: Symbol, 50 | set_led_fn: Symbol, 51 | device: DeviceHandle, 52 | } 53 | 54 | impl DirectOutput { 55 | /// Returns a new instance of the library loaded from its default 56 | /// installation location. Panics is the library cannot be loaded, e.g. not 57 | /// installed at the given location. 58 | pub fn load() -> Self { 59 | let library = Self::load_library(); 60 | let initialize_fn = Self::get_library_symbol(&library, b"DirectOutput_Initialize"); 61 | let enumerate_fn = Self::get_library_symbol(&library, b"DirectOutput_Enumerate"); 62 | let add_page_fn = Self::get_library_symbol(&library, b"DirectOutput_AddPage"); 63 | let set_led_fn = Self::get_library_symbol(&library, b"DirectOutput_SetLed"); 64 | 65 | Self { 66 | library, 67 | initialize_fn, 68 | enumerate_fn, 69 | add_page_fn, 70 | set_led_fn, 71 | device: std::ptr::null(), 72 | } 73 | } 74 | 75 | fn load_library() -> Library { 76 | let path = Self::directoutput_dll_path().expect( 77 | "Could not find path for DirectOutput.dll in registry; are the drivers installed?", 78 | ); 79 | 80 | unsafe { Library::new(path).expect("Could not load DirectOutput.dll") } 81 | } 82 | 83 | fn directoutput_dll_path() -> std::io::Result { 84 | let hklm = RegKey::predef(HKEY_LOCAL_MACHINE); 85 | let key = hklm.open_subkey(REGISTRY_KEY_PATH)?; 86 | let path: String = key.get_value(REGISTRY_KEY_NAME)?; 87 | debug!("DirectOutput DLL path = {:?}", path); 88 | Ok(path) 89 | } 90 | 91 | /// Given a function name returns a symbol for that function in the 92 | /// DirectOutput library. Panics if the symbol cannot be found. 93 | fn get_library_symbol(library: &Library, symbol: &[u8]) -> Symbol { 94 | unsafe { library.get::(symbol).unwrap().into_raw() } 95 | } 96 | 97 | /// Initializes the underlying library. This must be called before any 98 | /// other methods can be called. Panics if the initialization fails. 99 | pub fn initialize(&self) { 100 | unsafe { 101 | let result = (self.initialize_fn)(Self::win32_string(PLUGIN_NAME).as_ptr()); 102 | debug!("DirectOutput_Initialize result = {:?}", result); 103 | 104 | if result != 0 { 105 | panic!("Could not initialize the DirectOutput library"); 106 | } 107 | } 108 | } 109 | 110 | /// Enumerates the connected Saitek devices and selects the last given 111 | /// device. This wrapper does not give the ability to select a device by 112 | /// type or id but could be extended to do so. For the purposes of this 113 | /// project it is currently assuming that only X52Pro devices are attached, 114 | /// which may not be true in general. Panics if the enumeration fails. 115 | pub fn enumerate(&mut self) { 116 | extern "C" fn callback(device: DeviceHandle, target: &mut DirectOutput) { 117 | debug!("DirectOutput_Enumerate device = {:?}", device); 118 | target.device = device; 119 | } 120 | 121 | unsafe { 122 | let result = (self.enumerate_fn)(callback, self); 123 | debug!("DirectOutput_Enumerate result = {:?}", result); 124 | 125 | if result != 0 { 126 | panic!("Could not enumerate dervices with DirectOutput"); 127 | } 128 | } 129 | } 130 | 131 | /// Adds a display page to the device. This method must be called after 132 | /// `initialize` and before `set_led`. The underlying library supports 133 | /// multiple display pages that can be switched between but this wrapper 134 | /// creates a single page only. Panics if the addition fails. 135 | pub fn add_page(&self) { 136 | // Despite what the SDK documentation says, we have to pass in a non-null debug 137 | // name or later calls fail with an error indicating the page is not active. 138 | let debug_name = Self::win32_string(PLUGIN_NAME).as_ptr(); 139 | 140 | unsafe { 141 | let result = (self.add_page_fn)(self.device, PAGE_ID, debug_name, FLAG_SET_AS_ACTIVE); 142 | debug!("DirectOutput_AddPage result = {:?}", result); 143 | 144 | if result != 0 { 145 | panic!("Could not add page with DirectOutput"); 146 | } 147 | } 148 | } 149 | 150 | /// Activates or deactives the LED with the given `id` on the joystick. The 151 | /// `id` must be between 0 and 19 inclusive for the X52Pro. Panics if 152 | /// setting the LED state fails, e.g. if given an invalid `id`. 153 | pub fn set_led(&self, id: u32, active: bool) { 154 | let value = if active { 1 } else { 0 }; 155 | debug!("Setting LED {} to {}", id, value); 156 | 157 | unsafe { 158 | let result = (self.set_led_fn)(self.device, PAGE_ID, id, value); 159 | 160 | if result != 0 { 161 | panic!("Can't set LED, return value {}", result); 162 | } 163 | } 164 | } 165 | 166 | /// Given a native string `value` returns a Windows native "wide" string 167 | /// suitable for passing to Windows-native code. 168 | fn win32_string(value: &str) -> Vec { 169 | OsStr::new(value).encode_wide().chain(once(0)).collect() 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/x52pro/light_mode_to_state_mapper.rs: -------------------------------------------------------------------------------- 1 | use crate::x52pro::device::{BooleanLightMode, Led, RedAmberGreenLightMode}; 2 | use crate::x52pro::direct_output::DirectOutput; 3 | use std::time::SystemTime; 4 | 5 | pub const ALERT_FLASH_MILLISECONDS: u128 = 500; 6 | 7 | /// Available final, unanimated states for lights on the device. 8 | #[derive(Debug, PartialEq)] 9 | enum RedAmberGreenLightState { 10 | Off, 11 | Red, 12 | Amber, 13 | Green, 14 | } 15 | 16 | /// Available states for a light on the device that can be either off or on. 17 | #[derive(Debug, PartialEq)] 18 | enum BooleanLightState { 19 | Off, 20 | On, 21 | } 22 | 23 | /// Maps light modes to light states. The returned states change over time 24 | /// because certain modes are animated, i.e. flashing. 25 | pub struct LightModeToStateMapper { 26 | reference_time: SystemTime, 27 | } 28 | 29 | impl LightModeToStateMapper { 30 | /// Returns a new instance the mapper. 31 | pub fn new() -> Self { 32 | Self { 33 | reference_time: SystemTime::now(), 34 | } 35 | } 36 | 37 | /// Sets the given device LED to the correct state based on the given mode. 38 | pub fn update_binary_light( 39 | &self, 40 | direct_output: &DirectOutput, 41 | light_mode: &BooleanLightMode, 42 | led_id: Led, 43 | ) { 44 | let light_state = boolean_state_for_mode(light_mode, self.milliseconds_elapsed()); 45 | 46 | // Could move this mapping onto the enum. 47 | let led_active = match light_state { 48 | BooleanLightState::Off => false, 49 | BooleanLightState::On => true, 50 | }; 51 | 52 | direct_output.set_led(led_id as u32, led_active); 53 | } 54 | 55 | /// Sets the given device LEDs to the correct state based on the given mode. 56 | pub fn update_red_amber_green_light( 57 | &self, 58 | direct_output: &DirectOutput, 59 | light_mode: &RedAmberGreenLightMode, 60 | red_led_id: Led, 61 | green_led_id: Led, 62 | ) { 63 | let light_state = red_amber_green_state_for_mode(light_mode, self.milliseconds_elapsed()); 64 | 65 | // Could move this mapping onto the enum. 66 | let (red_led_state, green_led_state) = match light_state { 67 | RedAmberGreenLightState::Off => (false, false), 68 | RedAmberGreenLightState::Red => (true, false), 69 | RedAmberGreenLightState::Amber => (true, true), 70 | RedAmberGreenLightState::Green => (false, true), 71 | }; 72 | 73 | direct_output.set_led(red_led_id as u32, red_led_state); 74 | direct_output.set_led(green_led_id as u32, green_led_state); 75 | } 76 | 77 | /// Returns the number of milliseconds elapsed since the reference time. 78 | fn milliseconds_elapsed(&self) -> u128 { 79 | self.reference_time.elapsed().unwrap().as_millis() 80 | } 81 | } 82 | 83 | /// Returns the boolean light state that corrsponds to the given light mode at 84 | /// the given time offset (in milliseconds). 85 | fn boolean_state_for_mode(light_mode: &BooleanLightMode, milliseconds: u128) -> BooleanLightState { 86 | match light_mode { 87 | BooleanLightMode::Off => BooleanLightState::Off, 88 | BooleanLightMode::On => BooleanLightState::On, 89 | BooleanLightMode::Flash => { 90 | animated_state(milliseconds, BooleanLightState::On, BooleanLightState::Off) 91 | } 92 | } 93 | } 94 | 95 | /// Returns the red/amber/green light state that corrsponds to the given light 96 | /// mode at the given time offset (in milliseconds). 97 | fn red_amber_green_state_for_mode( 98 | light_mode: &RedAmberGreenLightMode, 99 | milliseconds: u128, 100 | ) -> RedAmberGreenLightState { 101 | use RedAmberGreenLightState::*; 102 | 103 | match light_mode { 104 | RedAmberGreenLightMode::Off => Off, 105 | RedAmberGreenLightMode::Red => Red, 106 | RedAmberGreenLightMode::Amber => Amber, 107 | RedAmberGreenLightMode::Green => Green, 108 | RedAmberGreenLightMode::RedAmber => animated_state(milliseconds, Red, Amber), 109 | RedAmberGreenLightMode::RedFlash => animated_state(milliseconds, Red, Off), 110 | RedAmberGreenLightMode::RedAmberFlash => animated_state(milliseconds, Red, Amber), 111 | RedAmberGreenLightMode::RedGreenFlash => animated_state(milliseconds, Red, Green), 112 | RedAmberGreenLightMode::AmberFlash => animated_state(milliseconds, Amber, Off), 113 | RedAmberGreenLightMode::AmberRedFlash => animated_state(milliseconds, Amber, Red), 114 | RedAmberGreenLightMode::AmberGreenFlash => animated_state(milliseconds, Amber, Green), 115 | RedAmberGreenLightMode::GreenFlash => animated_state(milliseconds, Green, Off), 116 | RedAmberGreenLightMode::GreenAmberFlash => animated_state(milliseconds, Green, Amber), 117 | RedAmberGreenLightMode::GreenRedFlash => animated_state(milliseconds, Green, Red), 118 | } 119 | } 120 | 121 | /// Returns either the first or second state based on the elapsed milliseconds 122 | /// given as compared to the defined interval for animation. 123 | fn animated_state(milliseconds: u128, first_state: T, second_state: T) -> T { 124 | if (milliseconds / ALERT_FLASH_MILLISECONDS) & 1 == 0 { 125 | first_state 126 | } else { 127 | second_state 128 | } 129 | } 130 | 131 | #[cfg(test)] 132 | mod tests { 133 | use super::*; 134 | 135 | fn assert_boolean_mapping( 136 | mode: BooleanLightMode, 137 | state_now: BooleanLightState, 138 | state_later: BooleanLightState, 139 | ) { 140 | assert_eq!(boolean_state_for_mode(&mode, 0), state_now); 141 | assert_eq!( 142 | boolean_state_for_mode(&mode, ALERT_FLASH_MILLISECONDS), 143 | state_later 144 | ); 145 | } 146 | 147 | #[test] 148 | fn boolean_light_states_for_modes() { 149 | use BooleanLightState::*; 150 | 151 | assert_boolean_mapping(BooleanLightMode::Off, Off, Off); 152 | assert_boolean_mapping(BooleanLightMode::On, On, On); 153 | assert_boolean_mapping(BooleanLightMode::Flash, On, Off); 154 | } 155 | 156 | fn assert_rag_mapping( 157 | mode: RedAmberGreenLightMode, 158 | state_now: RedAmberGreenLightState, 159 | state_later: RedAmberGreenLightState, 160 | ) { 161 | assert_eq!(red_amber_green_state_for_mode(&mode, 0), state_now); 162 | assert_eq!( 163 | red_amber_green_state_for_mode(&mode, ALERT_FLASH_MILLISECONDS), 164 | state_later 165 | ); 166 | } 167 | 168 | #[test] 169 | fn red_amber_green_states_for_modes() { 170 | use RedAmberGreenLightState::*; 171 | 172 | assert_rag_mapping(RedAmberGreenLightMode::Off, Off, Off); 173 | assert_rag_mapping(RedAmberGreenLightMode::Red, Red, Red); 174 | assert_rag_mapping(RedAmberGreenLightMode::Amber, Amber, Amber); 175 | assert_rag_mapping(RedAmberGreenLightMode::Green, Green, Green); 176 | assert_rag_mapping(RedAmberGreenLightMode::RedAmber, Red, Amber); 177 | assert_rag_mapping(RedAmberGreenLightMode::RedFlash, Red, Off); 178 | assert_rag_mapping(RedAmberGreenLightMode::RedAmberFlash, Red, Amber); 179 | assert_rag_mapping(RedAmberGreenLightMode::RedGreenFlash, Red, Green); 180 | assert_rag_mapping(RedAmberGreenLightMode::AmberFlash, Amber, Off); 181 | assert_rag_mapping(RedAmberGreenLightMode::AmberRedFlash, Amber, Red); 182 | assert_rag_mapping(RedAmberGreenLightMode::AmberGreenFlash, Amber, Green); 183 | assert_rag_mapping(RedAmberGreenLightMode::GreenFlash, Green, Off); 184 | assert_rag_mapping(RedAmberGreenLightMode::GreenAmberFlash, Green, Amber); 185 | assert_rag_mapping(RedAmberGreenLightMode::GreenRedFlash, Green, Red); 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /src/x52pro/status_level_to_mode_mapper.rs: -------------------------------------------------------------------------------- 1 | use crate::game::StatusLevel; 2 | use crate::x52pro::device::LightMode; 3 | 4 | /// Maps status levels to light modes based on the given configuration. 5 | #[derive(Debug, PartialEq)] 6 | pub struct StatusLevelToModeMapper { 7 | pub inactive: LightMode, 8 | pub active: LightMode, 9 | pub blocked: LightMode, 10 | pub alert: LightMode, 11 | } 12 | 13 | impl StatusLevelToModeMapper { 14 | /// Returns a new instance the mapper. 15 | pub fn new( 16 | inactive: LightMode, 17 | active: LightMode, 18 | blocked: LightMode, 19 | alert: LightMode, 20 | ) -> Self { 21 | Self { 22 | inactive, 23 | active, 24 | blocked, 25 | alert, 26 | } 27 | } 28 | 29 | pub fn map(&self, status_level: &StatusLevel) -> LightMode { 30 | match status_level { 31 | StatusLevel::Inactive => self.inactive, 32 | StatusLevel::Active => self.active, 33 | StatusLevel::Blocked => self.blocked, 34 | StatusLevel::Alert => self.alert, 35 | } 36 | } 37 | } 38 | --------------------------------------------------------------------------------