├── LICENSE ├── README.md ├── cfg └── valve.rc └── scripts └── vscripts ├── domtypes.nut ├── mapspawn.nut ├── ppmod.nut └── sl_httportal.nut /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HTTPortal 2 | Basic HTTP/1.1 server implementation in Portal 2. [See here](https://youtu.be/-v5vCLLsqbA) for a mid-level overview of the project. 3 | 4 | ## Usage 5 | - In Steam, add this launch option to Portal 2: `-netconport 3000` 6 | - Clone this repository and move its root directory to your game files. Rename it to `portal2_dlcX`, where X is the lowest positive integer not in use. 7 | - Start Portal 2, you should be brought to Laser Chaining after a couple of loading screens. 8 | - Enable the developer console in keyboard settings (if you haven't already). 9 | - Open `localhost:3000` in a browser - a blank page should open. 10 | 11 | ### Creating a webpage 12 | Here's a very quick crash course on how to use the page builder: 13 | - To create an HTML element (e.g. \), use this command: `script newElement(H1)` 14 | - To create a closing tag (e.g. \), use this command: `script newElement(-H1)` 15 | - To create a plaintext node, use this command: `script newElement("Hello, World!")` 16 | - To create a modifier (e.g. style), use this command: `script newModifier("style", "color: red")` 17 | 18 | Elements are ordered first towards -Y, then towards +X. In other words, if you're building your site on Laser Chaining, keep the faith plate platform to your right and place cubes left-to-right, top-to-bottom. 19 | 20 | The tag name must be provided in uppercase, _unquoted_ (it's an enumeration). For closing tags, just append a minus (`-`) to the tag name. 21 | 22 | Modifiers _stack_ - both literally and also in the document. So using two `style` modifiers on top of each other will stack the properties, but using two `src` or `href` modifiers will probably just break your link. 23 | 24 | ## Contributing 25 | Submit issues before pull requests (discuss, then code), and adhere to the code style. Ideally, use the [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/) naming scheme. Code that doesn't respect the style/conventions of the code base will not be accepted. 26 | -------------------------------------------------------------------------------- /cfg/valve.rc: -------------------------------------------------------------------------------- 1 | // default valve.rc contents 2 | exec joystick.cfg 3 | exec autoexec.cfg 4 | stuffcmds 5 | 6 | // a fresh start will likely try to rebuild soundcache 7 | // or, users may attempt to load a save, which skips ppmod.onauto 8 | // to work around both issues, we boot straight into a map 9 | map sp_a2_laser_chaining 10 | -------------------------------------------------------------------------------- /scripts/vscripts/domtypes.nut: -------------------------------------------------------------------------------- 1 | /** 2 | * Pseudo-enumeration for DOM element types. 3 | * These could easily be strings, but this is ever so slightly more convenient 4 | * in the developer console and in code. 5 | */ 6 | 7 | /** 8 | * Closing tags are represented as negative values. 9 | * For example, -38 represents the closing tag for

. 10 | * The 0th index is unused, as it can't be negated. 11 | */ 12 | ::ABBR <- 1; 13 | ::ADDRESS <- 2; 14 | ::AREA <- 3; 15 | ::ARTICLE <- 4; 16 | ::ASIDE <- 5; 17 | ::AUDIO <- 6; 18 | ::B <- 7; 19 | ::BASE <- 8; 20 | ::BDI <- 9; 21 | ::BDO <- 10; 22 | ::BLOCKQUOTE <- 11; 23 | ::BODY <- 12; 24 | ::BR <- 13; 25 | ::BUTTON <- 14; 26 | ::CANVAS <- 15; 27 | ::CAPTION <- 16; 28 | ::CITE <- 17; 29 | ::CODE <- 18; 30 | ::COL <- 19; 31 | ::COLGROUP <- 20; 32 | ::DATA <- 21; 33 | ::DATALIST <- 22; 34 | ::DD <- 23; 35 | ::DEL <- 24; 36 | ::DETAILS <- 25; 37 | ::DFN <- 26; 38 | ::DIALOG <- 27; 39 | ::DIV <- 28; 40 | ::DL <- 29; 41 | ::DT <- 30; 42 | ::EM <- 31; 43 | ::EMBED <- 32; 44 | ::FIELDSET <- 33; 45 | ::FIGCAPTION <- 34; 46 | ::FIGURE <- 35; 47 | ::FOOTER <- 36; 48 | ::FORM <- 37; 49 | ::H1 <- 38; 50 | ::H2 <- 39; 51 | ::H3 <- 40; 52 | ::H4 <- 41; 53 | ::H5 <- 42; 54 | ::H6 <- 43; 55 | ::HEAD <- 44; 56 | ::HEADER <- 45; 57 | ::HGROUP <- 46; 58 | ::HR <- 47; 59 | ::HTML <- 48; 60 | ::I <- 49; 61 | ::IFRAME <- 50; 62 | ::IMG <- 51; 63 | ::INPUT <- 52; 64 | ::INS <- 53; 65 | ::KBD <- 54; 66 | ::LABEL <- 55; 67 | ::LEGEND <- 56; 68 | ::LI <- 57; 69 | ::LINK <- 58; 70 | ::MAIN <- 59; 71 | ::MAP <- 60; 72 | ::MARK <- 61; 73 | ::MATH <- 62; 74 | ::MENU <- 63; 75 | ::META <- 64; 76 | ::METER <- 65; 77 | ::NAV <- 66; 78 | ::NOSCRIPT <- 67; 79 | ::OBJECT <- 68; 80 | ::OL <- 69; 81 | ::OPTGROUP <- 70; 82 | ::OPTION <- 71; 83 | ::OUTPUT <- 72; 84 | ::P <- 73; 85 | ::PICTURE <- 74; 86 | ::PRE <- 75; 87 | ::PROGRESS <- 76; 88 | ::Q <- 77; 89 | ::RB <- 78; 90 | ::RP <- 79; 91 | ::RT <- 80; 92 | ::RTC <- 81; 93 | ::RUBY <- 82; 94 | ::S <- 83; 95 | ::SAMP <- 84; 96 | ::SCRIPT <- 85; 97 | ::SECTION <- 86; 98 | ::SELECT <- 87; 99 | ::SLOT <- 88; 100 | ::SMALL <- 89; 101 | ::SOURCE <- 90; 102 | ::SPAN <- 91; 103 | ::STRONG <- 92; 104 | ::STYLE <- 93; 105 | ::SUB <- 94; 106 | ::SUMMARY <- 95; 107 | ::SUP <- 96; 108 | ::SVG <- 97; 109 | ::TABLE <- 98; 110 | ::TBODY <- 99; 111 | ::TD <- 100; 112 | ::TEMPLATE <- 101; 113 | ::TEXTAREA <- 102; 114 | ::TFOOT <- 103; 115 | ::TH <- 104; 116 | ::THEAD <- 105; 117 | ::TIME <- 106; 118 | ::TITLE <- 107; 119 | ::TR <- 108; 120 | ::TRACK <- 109; 121 | ::U <- 110; 122 | ::UL <- 111; 123 | ::VAR <- 112; 124 | ::VIDEO <- 113; 125 | ::WBR <- 114; 126 | ::A <- 115; 127 | 128 | // Used for converting enum to string 129 | ::DOM_ELEMENTS <- [ 130 | "UNUSED", 131 | "ABBR", 132 | "ADDRESS", 133 | "AREA", 134 | "ARTICLE", 135 | "ASIDE", 136 | "AUDIO", 137 | "B", 138 | "BASE", 139 | "BDI", 140 | "BDO", 141 | "BLOCKQUOTE", 142 | "BODY", 143 | "BR", 144 | "BUTTON", 145 | "CANVAS", 146 | "CAPTION", 147 | "CITE", 148 | "CODE", 149 | "COL", 150 | "COLGROUP", 151 | "DATA", 152 | "DATALIST", 153 | "DD", 154 | "DEL", 155 | "DETAILS", 156 | "DFN", 157 | "DIALOG", 158 | "DIV", 159 | "DL", 160 | "DT", 161 | "EM", 162 | "EMBED", 163 | "FIELDSET", 164 | "FIGCAPTION", 165 | "FIGURE", 166 | "FOOTER", 167 | "FORM", 168 | "H1", 169 | "H2", 170 | "H3", 171 | "H4", 172 | "H5", 173 | "H6", 174 | "HEAD", 175 | "HEADER", 176 | "HGROUP", 177 | "HR", 178 | "HTML", 179 | "I", 180 | "IFRAME", 181 | "IMG", 182 | "INPUT", 183 | "INS", 184 | "KBD", 185 | "LABEL", 186 | "LEGEND", 187 | "LI", 188 | "LINK", 189 | "MAIN", 190 | "MAP", 191 | "MARK", 192 | "MATH", 193 | "MENU", 194 | "META", 195 | "METER", 196 | "NAV", 197 | "NOSCRIPT", 198 | "OBJECT", 199 | "OL", 200 | "OPTGROUP", 201 | "OPTION", 202 | "OUTPUT", 203 | "P", 204 | "PICTURE", 205 | "PRE", 206 | "PROGRESS", 207 | "Q", 208 | "RB", 209 | "RP", 210 | "RT", 211 | "RTC", 212 | "RUBY", 213 | "S", 214 | "SAMP", 215 | "SCRIPT", 216 | "SECTION", 217 | "SELECT", 218 | "SLOT", 219 | "SMALL", 220 | "SOURCE", 221 | "SPAN", 222 | "STRONG", 223 | "STYLE", 224 | "SUB", 225 | "SUMMARY", 226 | "SUP", 227 | "SVG", 228 | "TABLE", 229 | "TBODY", 230 | "TD", 231 | "TEMPLATE", 232 | "TEXTAREA", 233 | "TFOOT", 234 | "TH", 235 | "THEAD", 236 | "TIME", 237 | "TITLE", 238 | "TR", 239 | "TRACK", 240 | "U", 241 | "UL", 242 | "VAR", 243 | "VIDEO", 244 | "WBR", 245 | "A" 246 | ]; 247 | 248 | // Used for coloring cubes in-game 249 | // Indices correspond to DOM_ELEMENTS and the enums 250 | ::DOM_COLORS <- [ 251 | "120 120 120", // UNUSED 252 | "102 204 255", // ABBR (Text-level semantics, light blue) 253 | "153 102 204", // ADDRESS (Grouping content, purple) 254 | "255 204 102", // AREA (Image maps, orange-yellow) 255 | "255 102 102", // ARTICLE (Sectioning content, red) 256 | "255 153 51", // ASIDE (Sectioning content, orange) 257 | "0 153 204", // AUDIO (Media, cool blue) 258 | "255 51 51", // B (Text-level semantics, bold red) 259 | "102 255 102", // BASE (Document metadata, bright green) 260 | "51 204 153", // BDI (Text-level semantics, teal) 261 | "51 153 204", // BDO (Text-level semantics, medium blue) 262 | "204 102 255", // BLOCKQUOTE (Grouping content, vibrant purple) 263 | "51 153 51", // BODY (Document structure, deep green) 264 | "204 204 204", // BR (Edits, light gray) 265 | "255 153 204", // BUTTON (Forms, pink) 266 | "255 102 255", // CANVAS (Graphics, bright pink/purple) 267 | "102 255 153", // CAPTION (Table content, mint green) 268 | "153 51 255", // CITE (Text-level semantics, dark purple) 269 | "51 204 255", // CODE (Text-level semantics, bright cyan) 270 | "204 153 102", // COL (Table content, brown-ish) 271 | "204 102 51", // COLGROUP (Table content, darker orange) 272 | "0 204 102", // DATA (Text-level semantics, emerald green) 273 | "153 255 51", // DATALIST (Forms, lime green) 274 | "255 204 51", // DD (List content, golden yellow) 275 | "255 102 153", // DEL (Edits, raspberry pink) 276 | "102 153 255", // DETAILS (Interactive elements, medium blue) 277 | "153 51 102", // DFN (Text-level semantics, deep rose) 278 | "255 51 153", // DIALOG (Interactive elements, hot pink) 279 | "153 204 255", // DIV (Grouping content, soft blue) 280 | "255 153 102", // DL (List content, peach) 281 | "255 102 51", // DT (List content, vivid orange) 282 | "204 255 51", // EM (Text-level semantics, bright yellow-green) 283 | "51 255 204", // EMBED (Embedded content, turquoise) 284 | "255 204 153", // FIELDSET (Forms, light peach) 285 | "255 153 153", // FIGCAPTION (Figure content, light red) 286 | "255 102 102", // FIGURE (Figure content, red) 287 | "102 102 255", // FOOTER (Sectioning content, medium purple) 288 | "255 255 102", // FORM (Forms, bright yellow) 289 | "255 0 0", // H1 (Sectioning content, primary red) 290 | "204 0 0", // H2 (Sectioning content, slightly darker red) 291 | "153 0 0", // H3 (Sectioning content, even darker red) 292 | "102 0 0", // H4 (Sectioning content, darker red) 293 | "51 0 0", // H5 (Sectioning content, deep red) 294 | "0 0 0", // H6 (Sectioning content, black for contrast) 295 | "153 255 153", // HEAD (Document metadata, light green) 296 | "102 255 102", // HEADER (Sectioning content, bright green) 297 | "51 255 51", // HGROUP (Sectioning content, vivid green) 298 | "102 102 102", // HR (Edits, dark gray) 299 | "51 102 51", // HTML (Document structure, dark green) 300 | "153 51 153", // I (Text-level semantics, purple-pink) 301 | "51 153 255", // IFRAME (Embedded content, sky blue) 302 | "0 204 204", // IMG (Embedded content, cyan) 303 | "255 255 0", // INPUT (Forms, yellow) 304 | "102 255 204", // INS (Edits, light teal) 305 | "255 153 51", // KBD (Text-level semantics, orange) 306 | "255 102 51", // LABEL (Forms, vivid orange) 307 | "255 51 102", // LEGEND (Forms, deep pink) 308 | "51 153 102", // LI (List content, green-teal) 309 | "51 51 204", // LINK (Document metadata, deep blue) 310 | "255 153 0", // MAIN (Sectioning content, dark orange) 311 | "204 255 102", // MAP (Image maps, lime yellow) 312 | "255 255 153", // MARK (Text-level semantics, light yellow) 313 | "153 102 255", // MATH (Embedded content, vibrant purple) 314 | "102 204 153", // MENU (List content, grayish green) 315 | "204 102 204", // META (Document metadata, medium purple) 316 | "102 255 51", // METER (Forms, bright green) 317 | "0 102 255", // NAV (Sectioning content, bright blue) 318 | "255 51 51", // NOSCRIPT (Scripting, red) 319 | "204 51 255", // OBJECT (Embedded content, bright purple) 320 | "102 153 51", // OL (List content, olive green) 321 | "102 102 51", // OPTGROUP (Forms, dark olive) 322 | "153 153 51", // OPTION (Forms, medium olive) 323 | "255 204 51", // OUTPUT (Forms, golden yellow) 324 | "102 102 255", // P (Grouping content, medium purple) 325 | "0 153 153", // PICTURE (Embedded content, deep teal) 326 | "51 51 153", // PRE (Text-level semantics, dark blue-purple) 327 | "51 255 153", // PROGRESS (Forms, vivid green) 328 | "255 51 204", // Q (Text-level semantics, fuchsia) 329 | "255 102 102", // RB (Ruby annotation, red) 330 | "255 153 102", // RP (Ruby annotation, peach) 331 | "255 204 102", // RT (Ruby annotation, orange-yellow) 332 | "255 51 51", // RTC (Ruby annotation, red) 333 | "204 51 51", // RUBY (Ruby annotation, darker red) 334 | "255 102 153", // S (Text-level semantics, raspberry pink) 335 | "153 51 51", // SAMP (Text-level semantics, dark red) 336 | "255 51 255", // SCRIPT (Scripting, bright pink/purple) 337 | "0 204 51", // SECTION (Sectioning content, vivid green) 338 | "102 153 204", // SELECT (Forms, grayish blue) 339 | "255 153 204", // SLOT (Web Components, pink) 340 | "153 204 255", // SMALL (Text-level semantics, soft blue) 341 | "0 255 255", // SOURCE (Media, bright cyan) 342 | "255 0 255", // SPAN (Text-level semantics, magenta) 343 | "204 0 204", // STRONG (Text-level semantics, dark magenta) 344 | "255 102 204", // STYLE (Document metadata, vibrant pink) 345 | "102 153 255", // SUB (Text-level semantics, medium blue) 346 | "255 153 102", // SUMMARY (Interactive elements, peach) 347 | "102 204 255", // SUP (Text-level semantics, light blue) 348 | "255 255 51", // SVG (Graphics, vibrant yellow) 349 | "102 51 0", // TABLE (Table content, dark brown) 350 | "153 102 51", // TBODY (Table content, medium brown) 351 | "204 153 102", // TD (Table content, lighter brown) 352 | "51 255 102", // TEMPLATE (Scripting, bright green) 353 | "204 102 153", // TEXTAREA (Forms, dusty pink) 354 | "102 102 51", // TFOOT (Table content, dark olive) 355 | "51 51 0", // TH (Table content, very dark olive) 356 | "51 51 0", // THEAD (Table header, very dark olive) 357 | "102 51 204", // TIME (Text-level semantics, deep purple) 358 | "51 102 153", // TITLE (Document metadata, grayish blue) 359 | "153 51 0", // TR (Table content, dark orange-brown) 360 | "51 204 102", // TRACK (Media, emerald green) 361 | "204 102 255", // U (Text-level semantics, vibrant purple) 362 | "51 102 102", // UL (List content, dark teal) 363 | "153 51 102", // VAR (Text-level semantics, deep rose) 364 | "0 102 204", // VIDEO (Media, cool blue) 365 | "204 204 204", // WBR (Text-level semantics, light gray) 366 | "0 0 255" // A (Text-level semantics, primary blue) 367 | ]; 368 | 369 | -------------------------------------------------------------------------------- /scripts/vscripts/mapspawn.nut: -------------------------------------------------------------------------------- 1 | if (!("Entities" in this)) return; 2 | 3 | // https://github.com/p2r3/ppmod 4 | IncludeScript("ppmod"); 5 | // https://github.com/p2r3/savelock 6 | IncludeScript("sl_httportal"); 7 | 8 | // Import DOM enums and colors 9 | IncludeScript("domtypes"); 10 | 11 | ::spawnHeldCube <- async(function (pos = null) { 12 | 13 | // Check if the player is already holding a cube 14 | if (pplayer.holding()) return; 15 | 16 | // Spawn a storage cube at the player's feet 17 | yield ppmod.give({ prop_weighted_cube = 1 }); 18 | local cube = yielded.prop_weighted_cube[0]; 19 | 20 | if (pos) { 21 | // If a position was provided, teleport the cube there 22 | cube.SetOrigin(pos); 23 | } else { 24 | // Otherwise, have the player pick up the cube 25 | cube.Use("", 0.0, GetPlayer(), GetPlayer()); 26 | } 27 | 28 | // This seems to be the only clean way to draw the cube's name on top of it 29 | SendToConsole("ent_messages " + cube.entindex()); 30 | 31 | return cube; 32 | 33 | }); 34 | 35 | /** 36 | * Creates a cube representing an HTML tag or plain text. 37 | * @param {string|number} type - The type of the element, either a string or a DOM enum. 38 | * @param {Vector|null} pos - The position to spawn the cube at, or null to spawn at the player's feet. 39 | * @returns {ppromise} A promise that resolves when the cube is spawned. 40 | */ 41 | ::newElement <- async(function (type, pos = null) { 42 | 43 | // Validate input type 44 | if (!(typeof type == "string") && !(abs(type) in DOM_ELEMENTS)) { 45 | printl("Unrecognized element type!"); 46 | return; 47 | } 48 | 49 | // Spawn a storage cube, get its handle 50 | yield spawnHeldCube(pos); 51 | local cube = yielded; 52 | 53 | // Store the cube's type in its script scope 54 | local scope = cube.GetScriptScope(); 55 | scope._domType <- type; 56 | 57 | if (typeof type == "string") { 58 | // Special case for raw strings 59 | cube.targetname = type; 60 | cube.Color("120 120 120"); 61 | } else { 62 | // Set the color and display name based on the type 63 | cube.Color(DOM_COLORS[abs(type)]); 64 | cube.targetname = "<" + (type < 0 ? "/" : "") + DOM_ELEMENTS[abs(type)] + ">"; 65 | } 66 | 67 | }); 68 | 69 | /** 70 | * Creates a modifier cube with the given name and data. 71 | * @param {string} name - The name of the modifier. 72 | * @param {string} data - The data associated with the modifier. 73 | * @param {Vector|null} pos - The position to spawn the cube at, or null to spawn at the player's feet. 74 | * @returns {ppromise} A promise that resolves when the cube is spawned. 75 | */ 76 | ::newModifier <- async(function (name, data, pos = null) { 77 | 78 | // Spawn a storage cube at the player's feet 79 | yield ppmod.give({ prop_weighted_cube = 1 }); 80 | local cube = yielded.prop_weighted_cube[0]; 81 | 82 | // Spawn a storage cube, get its handle 83 | yield spawnHeldCube(pos); 84 | local cube = yielded; 85 | 86 | // Store the modifier data in the cube's script scope 87 | local scope = cube.GetScriptScope(); 88 | scope._domModifier <- name; 89 | scope._domData <- data; 90 | 91 | // Set cube color and display name 92 | cube.Color("80 80 80"); 93 | cube.targetname = name + " = " + data; 94 | 95 | }); 96 | 97 | /** 98 | * Builds the HTML document from the cubes in the world. 99 | * @returns {string} The generated HTML document as a string. 100 | */ 101 | ::buildHTTP <- function () { 102 | 103 | /** 104 | * Holds the structure of the HTML document, for serializing later. 105 | * Honestly, this has very little to do with the actual HTML DOM, 106 | * but we do need to have iterable and sortable objects. 107 | */ 108 | local dom = []; 109 | 110 | // We're detaching on almost every step to avoid SQQuerySuspend on big pages 111 | ppmod.detach(function (args):(dom) { 112 | // Iterate over all non-modifier cubes and push them into the DOM 113 | while (args.cube = Entities.FindByClassname(args.cube, "prop_weighted_cube")) { 114 | 115 | local scope = args.cube.GetScriptScope(); 116 | if (!("_domType" in scope)) continue; 117 | 118 | dom.push({ 119 | pos = args.cube.GetOrigin(), 120 | type = scope._domType, 121 | modifiers = {} 122 | }); 123 | 124 | } 125 | }, { cube = null }); 126 | 127 | ppmod.detach(function (args):(dom) { 128 | // Iterate over all modifier cubes and push them onto their respective elements 129 | while (args.cube = Entities.FindByClassname(args.cube, "prop_weighted_cube")) { 130 | 131 | local scope = args.cube.GetScriptScope(); 132 | if (!("_domModifier" in scope)) continue; 133 | 134 | local pos = args.cube.GetOrigin(); 135 | 136 | foreach (obj in dom) { 137 | // Target cubes below us and within 18 units in X and Y 138 | // 18 units is the width of a regular storage cube 139 | if (fabs(obj.pos.x - pos.x) > 18.0) continue; 140 | if (fabs(obj.pos.y - pos.y) > 18.0) continue; 141 | // Set the modifier string, or append to it if it already exists 142 | if (!(scope._domModifier in obj.modifiers)) { 143 | obj.modifiers[scope._domModifier] <- scope._domData; 144 | } else { 145 | obj.modifiers[scope._domModifier] += ";" + scope._domData; 146 | } 147 | } 148 | 149 | } 150 | }, { cube = null }); 151 | 152 | // Sort the DOM elements, first by Y position, then by X position 153 | // This mimics the way HTML elements are rendered in a document 154 | dom.sort(function (a, b) { 155 | // The Y position has an 18 unit tolerance, to allow for human error 156 | // when manually placing cubes in the world 157 | if (a.pos.y > b.pos.y + 18.0) return -1; 158 | if (a.pos.y < b.pos.y - 18.0) return 1; 159 | if (a.pos.x > b.pos.x) return 1; 160 | if (a.pos.x < b.pos.x) return -1; 161 | return 0; 162 | }); 163 | 164 | // Silly object reference hack, Squirrel doesn't have closures 165 | local ref = { output = "" }; 166 | 167 | ppmod.detach(function (args):(dom, ref) { 168 | while (args.i < dom.len()) { 169 | local obj = dom[args.i]; 170 | 171 | // Build the modifiers string from the object's modifiers 172 | local modifiers = ""; 173 | foreach (key, value in obj.modifiers) { 174 | modifiers += key + "=\"" + value + "\" "; 175 | } 176 | 177 | // Write the object itself to the output string 178 | if (typeof obj.type == "string") { 179 | ref.output += obj.type; 180 | } else { 181 | ref.output += "<" + (obj.type < 0 ? "/" : "") + DOM_ELEMENTS[abs(obj.type)] + " " + modifiers + ">"; 182 | } 183 | 184 | args.i ++; 185 | } 186 | }, { i = 0 }); 187 | 188 | return ref.output; 189 | 190 | }; 191 | 192 | // Sends the generated HTML document to the client 193 | ::sendHTTP <- function (document) { 194 | 195 | // Convert to ppstring for couting newlines 196 | local str = ppstring(document); 197 | 198 | printl("HTTP/1.1 200 OK"); 199 | printl("Server: Portal 2"); 200 | printl("Content-Type: text/html"); 201 | printl("Content-Length: " + (str.len() + str.split("\n").len())); 202 | printl(""); 203 | 204 | // It seems like the console has some limit on how much it can print at once, 205 | // so we split the output into chunks of 1024 characters. 206 | for (local i = 0; i <= str.len() / 1024; i ++) { 207 | print(str.slice(i * 1024, min(i * 1024 + 1024, str.len()))); 208 | } 209 | 210 | // Double newline, just in case 211 | printl(""); 212 | printl(""); 213 | 214 | }; 215 | 216 | ppmod.onauto(function () { 217 | 218 | ::pplayer <- ppmod.player(GetPlayer()); 219 | 220 | // Register the "GET" command to build and send the HTML document 221 | ppmod.alias("GET", function () { 222 | local document = buildHTTP(); 223 | sendHTTP(document); 224 | }); 225 | 226 | // Required for ent_messages to work 227 | SendToConsole("developer 1"); 228 | SendToConsole("con_drawnotify 0"); 229 | 230 | }); 231 | -------------------------------------------------------------------------------- /scripts/vscripts/ppmod.nut: -------------------------------------------------------------------------------- 1 | /** 2 | * ppmod version 4 3 | * author: PortalRunner 4 | */ 5 | 6 | if (!("Entities" in this)) { 7 | throw "ppmod: Tried to run in a scope without CEntities!"; 8 | } 9 | 10 | if ("ppmod" in this) { 11 | printl("[ppmod] Warning: ppmod is already loaded!"); 12 | return; 13 | } 14 | 15 | ::ppmod <- {}; 16 | 17 | /********************/ 18 | // Global Utilities // 19 | /********************/ 20 | 21 | // Returns the smallest of two values 22 | ::min <- function (a, b) return a > b ? b : a; 23 | // Returns the largest of two values 24 | ::max <- function (a, b) return a < b ? b : a; 25 | // Rounds the input float, optionally to a set precision 26 | ::round <- function (a, b = 0) { 27 | if (b == 0) return floor(a + 0.5); 28 | return floor(a * (b = pow(10, b)) + 0.5) / b; 29 | } 30 | // Holds the "not a number" and "infinity" constants for comparison 31 | ::nan <- fabs(0.0 / 0.0); 32 | ::inf <- 1.0 / 0.0; 33 | 34 | // Extends the functionality of Squirrel arrays 35 | class pparray { 36 | 37 | arr = null; 38 | 39 | constructor (size = 0, fill = null) { 40 | if (typeof size == "array") arr = size; 41 | else arr = array(size, fill); 42 | } 43 | 44 | // Overload operators to mimic a standard array 45 | function _typeof () return "array"; 46 | function _get (idx) return arr[idx]; 47 | function _set (idx, val) return arr[idx] = val; 48 | function _nexti (previdx) { 49 | if (previdx < 0 || previdx >= this.len()) return null; 50 | if (previdx == null) return 0; 51 | return previdx + 1; 52 | } 53 | // Returns a representation of the array values as a string 54 | function _tostring () { 55 | local str = "["; 56 | for (local i = 0; i < arr.len(); i ++) { 57 | if (typeof arr[i] == "string") str += "\"" + arr[i] + "\""; 58 | else str += arr[i]; 59 | if (i != arr.len() - 1) str += ", "; 60 | } 61 | return str + "]"; 62 | } 63 | // Compares two arrays by their elements 64 | function _cmp (other) { 65 | local shortest = min(arr.len(), other.len()); 66 | for (local i = 0; i < shortest; i ++) { 67 | if (arr[i] < other[i]) return -1; 68 | else if (arr[i] > other[i]) return 1; 69 | } 70 | if (arr.len() < other.len()) return -1; 71 | if (arr.len() > other.len()) return 1; 72 | return 0; 73 | } 74 | 75 | // Implement standard Squirrel array methods 76 | function len () return arr.len(); 77 | function append (val) return arr.append(val); 78 | function push (val) return arr.push(val); 79 | function extend (other) return arr.extend(other); 80 | function pop () return arr.pop(); 81 | function top () return arr.top(); 82 | function insert (idx, val) return arr.insert(idx, val); 83 | function remove (idx) return arr.remove(idx); 84 | function resize (size, fill = null) return arr.resize(size, fill); 85 | function sort (func = null) return func ? arr.sort(func) : arr.sort(); 86 | function reverse () return arr.reverse(); 87 | function slice (start, end = null) return pparray(arr.slice(start, end || arr.len())); 88 | function tostring () return _tostring(); 89 | function clear () return arr.clear(); 90 | 91 | // Implement additional methods to extend array functionality 92 | function shift () return arr.remove(0); 93 | function unshift (val) return arr.insert(0, val); 94 | // Joins the elements of the array into a string 95 | function join (separator = ",") { 96 | local str = ""; 97 | for (local i = 0; i < arr.len(); i ++) { 98 | str += arr[i]; 99 | if (i != arr.len() - 1) str += separator; 100 | } 101 | return str; 102 | } 103 | // Checks if the contents of the two arrays are identical 104 | function equals (other) { 105 | if (arr.len() != other.len()) return 0; 106 | for (local i = 0; i < arr.len(); i ++) { 107 | if (typeof arr[i] == "array") { 108 | if (arr[i].equals(other[i]) == 0) return 0; 109 | } else { 110 | if (arr[i] != other[i]) return 0; 111 | } 112 | } 113 | return 1; 114 | } 115 | // Returns the index of the first element to match the input value 116 | // Returns -1 if no such element is found 117 | function indexof (match, start = 0) { 118 | for (local i = start; i < arr.len(); i ++) { 119 | if (arr[i] == match) return i; 120 | } 121 | return -1; 122 | } 123 | // Returns the index of the first element to pass the compare function 124 | function find (match, start = 0) { 125 | for (local i = start; i < arr.len(); i ++) { 126 | if (match(arr[i])) return i; 127 | } 128 | return -1; 129 | } 130 | // Returns true if the array contains the element, false otherwise 131 | function includes (match, start = 0) { 132 | return indexof(match, start) != -1; 133 | } 134 | 135 | } 136 | 137 | // Implements the heap data type 138 | class ppheap { 139 | 140 | arr = pparray([0]); 141 | size = 0; 142 | maxsize = 0; 143 | comp = null; 144 | 145 | constructor (maxs = 0, comparator = null) { 146 | maxsize = maxs; 147 | arr = pparray(maxsize * 4 + 1,0); 148 | if (comparator) { 149 | comp = comparator; 150 | } else { 151 | comp = function (a, b) { return a < b }; 152 | } 153 | } 154 | 155 | // Returns true if the heap is empty, false otherwise 156 | function isempty () return size == 0; 157 | // Sifts down the element at the given index to its correct position in the heap 158 | function bubbledown (hole) { 159 | local temp = arr[hole]; 160 | while (hole * 2 <= size) { 161 | local child = hole * 2; 162 | if (child != size && comp(arr[child + 1], arr[child])) child ++; 163 | if (comp(arr[child], temp)) { 164 | arr[hole] = arr[child] 165 | } else { 166 | break; 167 | } 168 | hole = child; 169 | } 170 | arr[hole] = temp; 171 | } 172 | // Removes the top element of the heap and returns it 173 | function remove () { 174 | if (isempty()) { 175 | throw "ppheap: Heap is empty"; 176 | } else { 177 | local tmp = arr[1]; 178 | arr[1] = arr[size--]; 179 | bubbledown(1); 180 | return tmp; 181 | } 182 | } 183 | // Returns the top element of the heap 184 | function gettop () { 185 | if (isempty()) { 186 | throw "ppheap: Heap is empty"; 187 | } else { 188 | return arr[1]; 189 | } 190 | } 191 | // Insers the given element into the heap 192 | function insert (val) { 193 | if (size == maxsize) { 194 | throw "ppheap: Exceeded max heap size"; 195 | } 196 | arr[0] = val; 197 | local hole = ++size; 198 | while (comp(val, arr[hole / 2])) { 199 | arr[hole] = arr[hole / 2]; 200 | hole /= 2; 201 | } 202 | arr[hole] = val; 203 | } 204 | 205 | } 206 | 207 | // Extends the functionality of Squirrel strings 208 | class ppstring { 209 | 210 | string = null; 211 | 212 | constructor (str = "") { 213 | string = str.tostring(); 214 | } 215 | 216 | // Overload operators to mimic a standard string 217 | function _typeof () return "string"; 218 | function _tostring () return string; 219 | function _add (other) return ppstring(string + other.tostring()); 220 | function _get (idx) return string[idx]; 221 | function _set (idx, val) return string = string.slice(0, idx) + val.tochar() + string.slice(idx + 1); 222 | function _cmp (other) { 223 | if (string == other.tostring()) return 0; 224 | if (string > other.tostring()) return 1; 225 | return -1; 226 | } 227 | 228 | // Implement standard Squirrel string methods 229 | function len () return string.len(); 230 | function tointeger () return string.tointeger(); 231 | function tofloat () return string.tofloat(); 232 | function tostring () return string; 233 | function slice (start, end = null) return ppstring(string.slice(start, end || string.len())); 234 | function find (substr, start = 0) return string.find(substr, start); 235 | function tolower () return ppstring(string.tolower()); 236 | function toupper () return ppstring(string.toupper()); 237 | function strip () return ppstring(::strip(string)); 238 | function lstrip () return ppstring(::lstrip(string)); 239 | function rstrip () return ppstring(::rstrip(string)); 240 | 241 | // Returns a string which replaces all occurrences of one substring with another 242 | function replace (substr, rep) { 243 | local out = "", prev = 0, idx = 0; 244 | while ((idx = string.find(substr, prev)) != null) { 245 | out += string.slice(prev, idx); 246 | out += rep; 247 | prev = idx + substr.len(); 248 | } 249 | return out + string.slice(prev); 250 | } 251 | // Returns a Squirrel array representing the string split up by a substring 252 | function split (substr) { 253 | local arr = [], curr = 0, prev = 0; 254 | while ((curr = string.find(substr, curr)) != null) { 255 | curr = max(curr, prev + 1); 256 | arr.push(string.slice(prev, curr)); 257 | prev = curr += substr.len(); 258 | } 259 | arr.push(string.slice(prev)); 260 | return arr; 261 | } 262 | // Returns true if the string includes the given substring 263 | function includes (substr, start = 0) { 264 | return string.find(substr, start) != null; 265 | } 266 | 267 | } 268 | 269 | /** 270 | * Because of a bug in how objects are restored from Portal 2 save files, 271 | * using a class for ppromise causes crashes on save load. Instead, we 272 | * mimic a class structure by returning a table from a function. 273 | */ 274 | 275 | // Methods for the ppromise prototypal class 276 | local ppromise_methods = { 277 | 278 | // Attaches a function to be executed when the promise fullfils 279 | then = function (onthen, oncatch = function (x) { throw x }) { 280 | if (typeof onthen != "function" || typeof oncatch != "function") { 281 | throw "ppromise: Invalid arguments for .then handler"; 282 | } 283 | 284 | // Run the function immediately if the promise has already fulfilled 285 | if (state == "fulfilled") { onthen(value); return this } 286 | if (state == "rejected") { oncatch(value); return this } 287 | 288 | onfulfill.push(onthen); 289 | onreject.push(oncatch); 290 | 291 | return this; 292 | }, 293 | // Attaches a function to be executed when the promise is rejected 294 | except = function (oncatch) { 295 | if (typeof oncatch != "function") { 296 | throw "ppromise: Invalid argument for .except handler"; 297 | } 298 | 299 | // Run the function immediately if the promise has already rejected 300 | if (state == "rejected") return oncatch(value); 301 | onreject.push(oncatch); 302 | 303 | return this; 304 | }, 305 | // Attaches a function to be executed when the promise resolves 306 | finally = function (onfinally) { 307 | if (typeof finally != "function") { 308 | throw "ppromise: Invalid argument for .finally handler"; 309 | } 310 | 311 | // Run the function immediately if the promise has already resolved 312 | if (state != "pending") return onfinally(value); 313 | onresolve.push(onfinally); 314 | 315 | return this; 316 | }, 317 | // Fulfills the given ppromise instance with the given value 318 | resolve = function (inst, val) { 319 | // If the promise has already been resolved, do nothing 320 | if (inst.state != "pending") return; 321 | 322 | // Update the promise state and value 323 | inst.state = "fulfilled"; 324 | inst.value = val; 325 | 326 | // Call all relevant functions attached to the promise 327 | for (local i = 0; i < inst.onfulfill.len(); i ++) inst.onfulfill[i](val); 328 | for (local i = 0; i < inst.onresolve.len(); i ++) inst.onresolve[i](); 329 | }, 330 | // Rejects the given ppromise instance with the given value 331 | reject = function (inst, err) { 332 | // If the promise has already been resolved, do nothing 333 | if (inst.state != "pending") return; 334 | 335 | // Update the promise state and value 336 | inst.state = "rejected"; 337 | inst.value = err; 338 | 339 | // If no error handler has been attached, throw the error 340 | if (inst.onreject.len() == 0) throw err; 341 | 342 | // Call all relevant functions attached to the promise 343 | for (local i = 0; i < inst.onreject.len(); i ++) inst.onreject[i](err); 344 | for (local i = 0; i < inst.onresolve.len(); i ++) inst.onresolve[i](); 345 | } 346 | 347 | } 348 | 349 | // Constructor for the ppromise prototypal class 350 | ::ppromise <- function (func):(ppromise_methods) { 351 | 352 | // Create a table to act as the class instance 353 | local inst = { 354 | 355 | onresolve = [], 356 | onfulfill = [], 357 | onreject = [], 358 | 359 | state = "pending", 360 | value = null, 361 | 362 | then = ppromise_methods.then, 363 | except = ppromise_methods.except, 364 | finally = ppromise_methods.finally 365 | 366 | resolve = null, 367 | reject = null 368 | 369 | }; 370 | 371 | // Wrappers for the resolve/reject handlers, capturing this instance 372 | inst.resolve = function (val = null):(ppromise_methods, inst) { 373 | ppromise_methods.resolve(inst, val); 374 | }; 375 | inst.reject = function (err = null):(ppromise_methods, inst) { 376 | ppromise_methods.reject(inst, err); 377 | }; 378 | 379 | // Run the input function 380 | try { func(inst.resolve, inst.reject) } 381 | catch (e) { inst.reject(e) } 382 | // Return the table representing a ppromise class instance 383 | return inst; 384 | 385 | } 386 | 387 | /** 388 | * Asynchronous functions are implemented using Squirrel generators. 389 | * Since a generator is essentially a function that can return some output 390 | * without exiting, we can use this property to suspend code execution 391 | * until another procedure is done processing the returned (yielded) 392 | * output, at which point the generator is told to resume. 393 | */ 394 | 395 | // Holds generators used for async functions 396 | ::ppmod.asyncgen <- []; 397 | // Holds the value of the last ppromise yielded from an async function 398 | ::yielded <- null; 399 | 400 | // Runs an async generator over and over until end of scope is reached 401 | ::ppmod.asyncrun <- function (id, resolve, reject):(ppromise_methods) { 402 | 403 | // Holds the yielded/returned value of the generator 404 | local next; 405 | try { next = resume ppmod.asyncgen[id] } 406 | catch (e) { return reject(e) } 407 | 408 | // If the generator has finished running, resolve the async function 409 | if (ppmod.asyncgen[id].getstatus() == "dead") { 410 | ppmod.asyncgen[id] = null; 411 | return resolve(next); 412 | } 413 | 414 | // Ensure we're handling a ppromise instance 415 | if (next.then != ppromise_methods.then) { 416 | throw "async: Function did not yield a ppromise"; 417 | } 418 | // Resume the generator when the promise resolves 419 | next.then(function (val):(id, resolve, reject) { 420 | ::yielded <- val; 421 | ppmod.asyncrun(id, resolve, reject); 422 | }); 423 | 424 | } 425 | 426 | // Converts a function to one that returns a ppromise 427 | ::async <- function (func) { 428 | return function (...):(func) { 429 | 430 | // Extract the arguments and format them for acall() 431 | local args = array(vargc + 1); 432 | for (local i = 0; i < vargc; i ++) args[i + 1] = vargv[i]; 433 | args[0] = this; 434 | 435 | // Create a ppromise which runs the input function as a generator 436 | return ppromise(function (resolve, reject):(func, args) { 437 | // Find a free spot in ppmod.asyncgen to insert this function 438 | for (local i = 0; i < ppmod.asyncgen.len(); i ++) { 439 | if (ppmod.asyncgen[i] == null) { 440 | ppmod.asyncgen[i] = func.acall(args); 441 | ppmod.asyncrun(i, resolve, reject); 442 | return; 443 | } 444 | } 445 | // If no free space was found, extend the array by pushing to it 446 | ppmod.asyncgen.push(func.acall(args)); 447 | ppmod.asyncrun(ppmod.asyncgen.len() - 1, resolve, reject); 448 | }); 449 | 450 | }; 451 | } 452 | 453 | // Extend Vector class functionality 454 | try { 455 | // Implement multiplication with other Vectors 456 | function Vector::_mul (other) { 457 | if (typeof other == "Vector") { 458 | return Vector(this.x * other.x, this.y * other.y, this.z * other.z); 459 | } else { 460 | return Vector(this.x * other, this.y * other, this.z * other); 461 | } 462 | } 463 | // Implement component-wise division with numbers and Vectors 464 | function Vector::_div (other) { 465 | if (typeof other == "Vector") { 466 | return Vector(this.x / other.x, this.y / other.y, this.z / other.z); 467 | } else { 468 | return Vector(this.x / other, this.y / other, this.z / other); 469 | } 470 | } 471 | // Implement unary minus 472 | function Vector::_unm () { 473 | return Vector() - this; 474 | } 475 | // Returns true if the components of the two vectors are identical, false otherwise 476 | function Vector::equals (other) { 477 | if (this.x == other.x && this.y == other.y && this.z == other.z) return true; 478 | return false; 479 | } 480 | // Returns a string representation of the Vector as a Vector constructor 481 | function Vector::_tostring () { 482 | return "Vector(" + this.x + ", " + this.y + ", " + this.z + ")"; 483 | } 484 | // Fixes the built-in ToKVString function by reimplementing it 485 | function Vector::ToKVString () { 486 | return this.x + " " + this.y + " " + this.z; 487 | } 488 | // Normalizes the vector and returns it 489 | function Vector::Normalize () { 490 | this.Norm(); 491 | return this; 492 | } 493 | // Normalizes the vector along just the X/Y axis and returns it 494 | function Vector::Normalize2D () { 495 | this.z = 0.0; 496 | this.Norm(); 497 | return this; 498 | } 499 | // Creates a deep copy of the vector and returns it 500 | function Vector::Copy () { 501 | return Vector(this.x, this.y, this.z); 502 | } 503 | // Converts the direction vector(s) to a vector of pitch/yaw/roll angles 504 | function Vector::ToAngles (uvec = null, rad = false) { 505 | // Copy and normalize the forward vector (`this`) 506 | local fvec = this.Copy(); 507 | fvec.Norm(); 508 | // Calculate yaw/pitch angles 509 | local yaw = atan2(fvec.y, fvec.x); 510 | local pitch = asin(-fvec.z); 511 | local roll = 0.0; 512 | // If an up vector is given, calculate roll 513 | // Reference: https://www.jldoty.com/code/DirectX/YPRfromUF/YPRfromUF.html 514 | if (typeof uvec == "Vector") { 515 | // Copy and normalize the input up vector 516 | uvec = uvec.Copy(); 517 | uvec.Norm(); 518 | // Calculate the current right vector 519 | local rvec = uvec.Cross(fvec).Normalize(); 520 | // Ensure the up vector is orthonormal 521 | uvec = fvec.Cross(rvec).Normalize(); 522 | // Calculate right/up vectors at zero roll 523 | local x0 = Vector(0, 0, 1).Cross(fvec).Normalize(); 524 | local y0 = fvec.Cross(x0); 525 | // Calculate the sine and cosine of the roll angle 526 | local rollcos = y0.Dot(uvec); 527 | local rollsin; 528 | if (fabs(fabs(fvec.z) - 1.0) < 0.000001) { 529 | // Edge case for the fvec.z +/- 1.0 singularity 530 | rollsin = -uvec.x; 531 | } else { 532 | // Choose a denominator that won't divide by zero 533 | local s = Vector(fabs(x0.x), fabs(x0.y), fabs(x0.z)); 534 | local c = (s.x > s.y) ? (s.x > s.z ? "x" : "z") : (s.y > s.z ? "y" : "z"); 535 | // Calculate the roll angle sine 536 | rollsin = (y0[c] * rollcos - uvec[c]) / x0[c]; 537 | } 538 | // Calculate the signed roll angle 539 | roll = atan2(rollsin, rollcos); 540 | } 541 | // Return angles as a pitch/yaw/roll vector 542 | if (rad) return Vector(pitch, yaw, roll); 543 | return Vector(pitch, yaw, roll) * (180.0 / PI); 544 | } 545 | // Given a vector of pitch/yaw/roll angles, returns forward and up vectors 546 | function Vector::FromAngles (rad = false) { 547 | // Convert degrees to radians if necessary 548 | local ang = this; 549 | if (!rad) ang = this.Copy() * PI / 180.0; 550 | // Precompute sines and cosines of angles 551 | local cy = cos(ang.y), sy = sin(ang.y); 552 | local cp = cos(ang.x), sp = sin(ang.x); 553 | local cr = cos(ang.z), sr = sin(ang.z); 554 | // Calculate the forward and up vectors 555 | return { 556 | fvec = Vector(cy * cp, sy * cp, -sp), 557 | uvec = Vector(cy * sp * sr - sy * cr, sy * sp * sr + cy * cr, cp * sr) 558 | }; 559 | } 560 | } catch (e) { 561 | printl("[ppmod] Warning: failed to modify Vector class: " + e); 562 | } 563 | 564 | /*********************/ 565 | // Entity management // 566 | /*********************/ 567 | 568 | // Finds an entity which matches the given parameters 569 | ::ppmod.get <- function (arg1, arg2 = null, arg3 = null, arg4 = null) { 570 | 571 | // Entity iterator 572 | local curr = null; 573 | 574 | // The type of the first argument determines the operation 575 | switch (typeof arg1) { 576 | 577 | case "string": { 578 | // Try to first find a match by targetname 579 | if (curr = Entities.FindByName(arg2, arg1)) return curr; 580 | // Fall back to a match by classname 581 | if (curr = Entities.FindByClassname(arg2, arg1)) return curr; 582 | // Fall back to a match by model name 583 | return Entities.FindByModel(arg2, arg1); 584 | } 585 | 586 | case "Vector": { 587 | // The second argument is the radius, 32u by default 588 | if (arg2 == null) arg2 = 32.0; 589 | 590 | // The filter argument is optional, and thus the starting entity 591 | // may be in either the third or fourth position. This makes sure 592 | // that it is always in arg4. 593 | if (typeof arg3 == "instance" && arg3 instanceof CBaseEntity) { 594 | arg4 = arg3; 595 | } 596 | 597 | // Validate the starting entity (fourth argument) 598 | if (arg4 != null && !(typeof arg4 == "instance" && arg4 instanceof CBaseEntity)) { 599 | throw "get: Invalid starting entity"; 600 | } 601 | 602 | // If no valid filter was provided, get the first entity in the radius 603 | if (typeof arg3 != "string") { 604 | return Entities.FindInSphere(arg4, arg1, arg2); 605 | } 606 | 607 | // If a filter was provided, find an entity in the radius that matches it 608 | while (arg4 = Entities.FindInSphere(arg4, arg1, arg2)) { 609 | if (!arg4.IsValid()) continue; 610 | if (arg4.GetName() == arg3 || arg4.GetClassname() == arg3 || arg4.GetModelName() == arg3) { 611 | return arg4; 612 | } 613 | } 614 | // Return null if nothing was found 615 | return null; 616 | } 617 | 618 | case "integer": { 619 | // Iterate through all entities to find a matching entindex 620 | while (curr = Entities.Next(curr)) { 621 | if (!curr.IsValid()) continue; 622 | if (curr.entindex() == arg1) return curr; 623 | } 624 | // Return null if no such entity exists 625 | return null; 626 | } 627 | 628 | case "instance": { 629 | // If provided an entity, validate it and echo it back 630 | if (ppmod.validate(arg1)) return arg1; 631 | else return null; 632 | } 633 | 634 | default: 635 | throw "get: Invalid first argument"; 636 | 637 | } 638 | 639 | } 640 | 641 | // Returns true if the input is a valid entity handle, false otherwise 642 | ::ppmod.validate <- function (ent) { 643 | // Entity handles must be of type "instance" 644 | if (typeof ent != "instance") return false; 645 | // Entity handles must be instances of CBaseEntity 646 | if (ent instanceof CBaseEntity) return ent.IsValid(); 647 | return false; 648 | } 649 | 650 | // Iterates through all entities that match the given criteria 651 | ::ppmod.forent <- function (args, callback) { 652 | 653 | // Convert the input to an array if it isn't already 654 | if (typeof args != "array") args = [args]; 655 | // Prepare args for use with acall() 656 | args.insert(0, this); 657 | 658 | // If the last argument is not a valid starting entity, push null 659 | local last = args.len() - 1; 660 | if (!ppmod.validate(args[last]) && args[last] != null) { 661 | args.push(null); 662 | last ++; 663 | } 664 | 665 | // Iterate through entities, running the callback on each valid one 666 | while (args[last] = ppmod.get.acall(args)) { 667 | if (!args[last].IsValid()) continue; 668 | callback(args[last]); 669 | } 670 | 671 | } 672 | 673 | // Iterates over entities backwards using ppmod.get 674 | ::ppmod.prev <- function (...) { 675 | 676 | // Set up entity iterators 677 | local start = null, curr = null, prev = null; 678 | 679 | // If the last argument is a valid starting entity, assign it 680 | if (ppmod.validate(vargv[vargc - 1])) { 681 | start = vargv[vargc - 1]; 682 | curr = start; 683 | } 684 | 685 | do { 686 | // Keep track of the entity from the previous iteration 687 | prev = curr; 688 | // Because vargv isn't a typical array, we can't use acall() here 689 | if (vargc < 3) curr = ppmod.get(vargv[0], curr); 690 | else if (vargc == 3) curr = ppmod.get(vargv[0], vargv[1], curr); 691 | else curr = ppmod.get(vargv[0], vargv[1], vargv[2], curr); 692 | // Run until we end up where we started 693 | } while (curr != start); 694 | 695 | // Return the entity from the last iteration 696 | return prev; 697 | 698 | } 699 | 700 | // Calls an input on an entity with optional default arguments 701 | ::ppmod.fire <- function (ent, action = "Use", value = "", delay = 0.0, activator = null, caller = null) { 702 | 703 | // If a string was provided, use DoEntFire 704 | if (typeof ent == "string") { 705 | return DoEntFire(ent, action, value.tostring(), delay, activator, caller); 706 | } 707 | // If an entity handle was provided, use EntFireByHandle 708 | if (typeof ent == "instance" && ent instanceof CBaseEntity) { 709 | if (!ent.IsValid()) throw "fire: Invalid entity handle"; 710 | return EntFireByHandle(ent, action, value.tostring(), delay, activator, caller); 711 | } 712 | // If any other argument was provided, use ppmod.forent to search for handles 713 | ppmod.forent(ent, function (curr):(action, value, delay, activator, caller) { 714 | ppmod.fire(curr, action, value, delay, activator, caller); 715 | }); 716 | 717 | } 718 | 719 | // Sets an entity keyvalue by automatically determining input type 720 | ::ppmod.keyval <- function (ent, key, val) { 721 | 722 | // Validate the key argument 723 | if (typeof key != "string") throw "keyval: Invalid key argument"; 724 | 725 | // If not provided with an entity handle, use ppmod.forent to search for handles 726 | if (!ppmod.validate(ent)) { 727 | return ppmod.forent(ent, function (curr):(key, val) { 728 | ppmod.keyval(curr, key, val); 729 | }); 730 | } 731 | 732 | // Use the appropriate method based on input type 733 | switch (typeof val) { 734 | 735 | case "integer": 736 | case "bool": 737 | ent.__KeyValueFromInt(key, val.tointeger()); 738 | break; 739 | case "float": 740 | ent.__KeyValueFromFloat(key, val); 741 | break; 742 | case "Vector": 743 | ent.__KeyValueFromVector(key, val); 744 | break; 745 | default: 746 | ent.__KeyValueFromString(key, val.tostring()); 747 | 748 | } 749 | 750 | } 751 | 752 | // Sets entity spawn flags from the argument list 753 | ::ppmod.flags <- function (ent, ...) { 754 | 755 | // Sum up all entries in vargv 756 | local sum = 0; 757 | for (local i = 0; i < vargc; i ++) { 758 | sum += vargv[i]; 759 | } 760 | 761 | // Call ppmod.keyval to apply the SpawnFlags keyvalue 762 | ppmod.keyval(ent, "SpawnFlags", sum); 763 | 764 | } 765 | 766 | // Creates an output to fire on the specified target with optional default arguments 767 | ::ppmod.addoutput <- function (ent, output, target, input = "Use", value = "", delay = 0, max = -1) { 768 | 769 | // If the target is not a string, wrap a ppmod.fire call inside of 770 | // ppmod.addscript to simulate an output whose target is a ppmod.forent argument. 771 | if (typeof target != "string") { 772 | return ppmod.addscript(ent, output, function ():(target, input, value) { 773 | ppmod.fire(target, input, value, 0.0, activator, caller); 774 | }, delay, max); 775 | } 776 | // Otherwise, assign the output as a keyvalue separated by x1B characters. 777 | // This seems to be how entity outputs are represented internally, and 778 | // should in theory be faster and safer than using the AddOutput input. 779 | ppmod.keyval(ent, output, target+"\x1B"+input+"\x1B"+value+"\x1B"+delay+"\x1B"+max); 780 | 781 | } 782 | 783 | // Keep track of a "script queue" for inline functions 784 | // This is used to keep global references to functions for use as callbacks 785 | ::ppmod.scrq <- []; 786 | 787 | // Adds a function to the script queue, returns its script queue index 788 | ::ppmod.scrq_add <- function (scr, max = -1) { 789 | 790 | // If the input is a string, compile it into a function 791 | if (typeof scr == "string") scr = compilestring(scr); 792 | // Validate the input script argument 793 | if (typeof scr != "function") throw "scrq_add: Invalid script argument"; 794 | 795 | // Look for an free space in the script queue array 796 | for (local i = 0; i < ppmod.scrq.len(); i ++) { 797 | if (ppmod.scrq[i] == null) { 798 | ppmod.scrq[i] = [scr, max]; 799 | return i; 800 | } 801 | } 802 | // If no free space was found, push it to the end of the array 803 | ppmod.scrq.push([scr, max]); 804 | return ppmod.scrq.len() - 1; 805 | 806 | } 807 | 808 | // Retrieves a function from the script queue, deleting it if needed 809 | ::ppmod.scrq_get <- function (idx) { 810 | 811 | // Validate the input script index 812 | if (!(idx in ppmod.scrq)) throw "scrq_get: Invalid script index"; 813 | if (ppmod.scrq[idx] == null) throw "scrq_get: Invalid script index"; 814 | 815 | // Retrieve the function from the queue 816 | local scr = ppmod.scrq[idx][0]; 817 | 818 | // Clear the script queue index if the max amount of retrievals has been reached 819 | if (ppmod.scrq[idx][1] > 0 && --ppmod.scrq[idx][1] == 0) { 820 | ppmod.scrq[idx] = null; 821 | } 822 | 823 | // Return the script queue function 824 | return scr; 825 | 826 | } 827 | 828 | // Adds a script as an output to an entity with optional default arguments 829 | ::ppmod.addscript <- function (ent, output, scr = "", delay = 0, max = -1) { 830 | 831 | if (typeof scr == "function") { 832 | // If a function was provided, add it to the script queue 833 | local scrq_idx = ppmod.scrq_add(scr, max); 834 | local scrq_arr = ppmod.scrq[scrq_idx]; 835 | // Attach a destructor to clear the scrq entry when the entity dies 836 | ppmod.onkill(ent, function ():(scrq_idx, scrq_arr) { 837 | if (ppmod.scrq[scrq_idx] != scrq_arr) return; 838 | ppmod.scrq[scrq_idx] = null; 839 | }); 840 | // Convert the argument to a scrq_get call string 841 | scr = "ppmod.scrq_get(" + scrq_idx + ")()"; 842 | } 843 | // Attach the output as a keyvalue, similar to how ppmod.addoutput does it 844 | // The script is targeted to worldspawn, as that makes activator and caller available 845 | ppmod.keyval(ent, output, "worldspawn\x001BRunScriptCode\x1B"+scr+"\x1B"+delay+"\x1B"+max); 846 | 847 | } 848 | 849 | // Runs the specified script in the entity's script scope 850 | ::ppmod.runscript <- function (ent, scr) { 851 | 852 | // If a function was provided, add it to the script queue 853 | if (typeof scr == "function") { 854 | scr = "ppmod.scrq_get(" + ppmod.scrq_add(scr, 1) + ")()"; 855 | } 856 | // Fire the RunScriptCode output on the input entity 857 | ppmod.fire(ent, "RunScriptCode", scr); 858 | 859 | } 860 | 861 | // Assigns or clears the movement parent of an entity 862 | ::ppmod.setparent <- function (child, _parent) { 863 | 864 | // If the new parent value is falsy, clear the parent 865 | if (!_parent) return ppmod.fire(child, "ClearParent"); 866 | // Validate the parent handle 867 | if (!ppmod.validate(_parent)) throw "setparent: Invalid parent handle"; 868 | // If a valid parent handle was provided, assign the parent 869 | return ppmod.fire(child, "SetParent", "!activator", 0, _parent); 870 | 871 | } 872 | 873 | // Iterates over the children of an entity 874 | ::ppmod.getchild <- function (_parent, ent = null) { 875 | 876 | // Validate input arguments 877 | if (!ppmod.validate(_parent)) throw "getchild: Invalid parent entity"; 878 | if (ent != null && !ppmod.validate(ent)) throw "getchild: Invalid iterator entity"; 879 | 880 | // Iterate over all world entities, looking for those with a common parent 881 | while (ent = Entities.Next(ent)) { 882 | if (!ent.IsValid()) continue; 883 | if (ent.GetMoveParent() != _parent) continue; 884 | return ent; 885 | } 886 | return ent; 887 | 888 | } 889 | 890 | // Hooks an entity input, running a test function each time it's fired 891 | ::ppmod.hook <- function (ent, input, scr, max = -1) { 892 | 893 | // Validate arguments 894 | if (typeof input != "string") throw "hook: Invalid input argument"; 895 | if (typeof max != "integer") throw "hook: Invalid max argument"; 896 | if (typeof scr == "string") scr = compilestring(scr); 897 | if (scr != null && typeof scr != "function") throw "hook: Invalid script argument"; 898 | // If a valid entity handle was not provided, find handles with ppmod.forent 899 | if (!ppmod.validate(ent)) { 900 | return ppmod.forent(ent, function (curr):(input, scr, max) { 901 | ppmod.hook(curr, input, scr, max); 902 | }); 903 | } 904 | // Ensure a script scope exists for the entity 905 | if (!ent.ValidateScriptScope()) { 906 | throw "hook: Could not validate entity script scope"; 907 | } 908 | // If the new script is null, clear the hook 909 | if (scr == null) delete ent.GetScriptScope()["Input"+input]; 910 | // Otherwise, assign a new hook function 911 | else ent.GetScriptScope()["Input"+input] <- scr; 912 | 913 | } 914 | 915 | // Attaches a function to be called when the entity is Kill-ed 916 | ::ppmod.onkill <- function (ent, scr) { 917 | 918 | // Validate arguments 919 | if (typeof scr == "string") scr = compilestring(scr); 920 | if (typeof scr != "function") throw "onkill: Invalid script argument"; 921 | 922 | // If a valid entity handle was not provided, find handles with ppmod.forent 923 | if (!ppmod.validate(ent)) { 924 | return ppmod.forent(ent, function (curr):(scr) { 925 | ppmod.onkill(curr, scr); 926 | }); 927 | } 928 | 929 | // Create and retrieve the entity's script scope 930 | if (!ent.ValidateScriptScope()) throw "onkill: Failed to create entity script scope"; 931 | local scope = ent.GetScriptScope(); 932 | 933 | if (!("__destructors" in scope)) { 934 | // If this is the first destructor, initialize an array 935 | scope.__destructors <- []; 936 | // Hook the "Kill" and "KillHierarchy" inputs to call destructors 937 | scope.InputKill <- function ():(scope) { 938 | for (local i = 0; i < scope.__destructors.len(); i ++) { 939 | scope.__destructors[i](); 940 | } 941 | return true; 942 | }; 943 | scope.InputKillHierarchy <- scope.InputKill; 944 | } 945 | 946 | // Push the new destructor to the entity's destructors array 947 | scope.__destructors.push(scr); 948 | 949 | } 950 | 951 | // Implement shorthands of the above functions into the entities as methods 952 | local entclasses = [CBaseEntity, CBaseAnimating, CBaseFlex, CBasePlayer, CEnvEntityMaker, CLinkedPortalDoor, CPortal_Player, CPropLinkedPortalDoor, CSceneEntity, CTriggerCamera]; 953 | for (local i = 0; i < entclasses.len(); i ++) { 954 | try { 955 | // Allows for setting keyvalues as if they were object properties 956 | entclasses[i]._set <- function (key, val) { 957 | // This is mostly identical to ppmod.keyval 958 | // However, having this be separate is slightly more performant 959 | if (typeof key != "string") throw "Invalid slot name"; 960 | switch (typeof val) { 961 | case "integer": 962 | case "bool": 963 | this.__KeyValueFromInt(key, val.tointeger()); 964 | break; 965 | case "float": 966 | this.__KeyValueFromFloat(key, val); 967 | break; 968 | case "Vector": 969 | this.__KeyValueFromVector(key, val); 970 | break; 971 | default: 972 | this.__KeyValueFromString(key, val.tostring()); 973 | } 974 | return val; 975 | } 976 | // Allows for firing inputs/connecting outputs as if they were methods 977 | entclasses[i]._get <- function (key) { 978 | return function (value = "", delay = 0.0, activator = null, caller = null):(key) { 979 | // If a function was provided, treat `key` as an output 980 | if (typeof value == "function") return ::ppmod.addscript(this, key, value, delay, activator); 981 | // Otherwise, treat `key` as an input 982 | return ::EntFireByHandle(this, key, value.tostring(), delay, activator, caller); 983 | } 984 | } 985 | // Self-explanatory wrappers for ppmod functions 986 | entclasses[i].Fire <- function (action = "Use", value = "", delay = 0.0, activator = null, caller = null) { 987 | return ::EntFireByHandle(this, action, value.tostring(), delay, activator, caller); 988 | } 989 | entclasses[i].AddOutput <- function (output, target, input = "Use", value = "", delay = 0, max = -1) { 990 | return ::ppmod.addoutput(this, output, target, input, value, delay, max); 991 | } 992 | entclasses[i].AddScript <- function (output, scr = "", delay = 0, max = -1) { 993 | return ::ppmod.addscript(this, output, scr, delay, max); 994 | } 995 | entclasses[i].RunScript <- function (scr) { 996 | return ::ppmod.runscript(this, scr); 997 | } 998 | entclasses[i].SetMoveParent <- function (_parent) { 999 | return ::ppmod.setparent(this, _parent); 1000 | } 1001 | entclasses[i].NextMoveChild <- function (child = null) { 1002 | return ::ppmod.getchild(this, child); 1003 | } 1004 | entclasses[i].SetHook <- function (input, scr, max = -1) { 1005 | return ::ppmod.hook(this, input, scr, max); 1006 | } 1007 | entclasses[i].OnKill <- function (scr) { 1008 | return ::ppmod.onkill(this, scr); 1009 | } 1010 | // Overwrite GetScriptScope to first create/validate the scope 1011 | // This makes it safer and more comfortable to to access script scopes 1012 | entclasses[i].DoGetScriptScope <- entclasses[i].GetScriptScope; 1013 | entclasses[i].GetScriptScope <- function () { 1014 | if (!this.ValidateScriptScope()) throw "Could not validate entity script scope"; 1015 | return this.DoGetScriptScope(); 1016 | } 1017 | // Overwrite SetAngles to sanitize angles and support Vector input 1018 | entclasses[i].DoSetAngles <- entclasses[i].SetAngles; 1019 | entclasses[i].SetAngles <- function (pitch, yaw = 0.0, roll = 0.0) { 1020 | // Support input of a PYR Vector 1021 | if (typeof pitch == "Vector") { 1022 | yaw = pitch.y; 1023 | roll = pitch.z; 1024 | pitch = pitch.x; 1025 | } 1026 | // Ensure the input angles are valid 1027 | if (::fabs(pitch) == nan || ::fabs(pitch) == inf) throw "Invalid pitch angle - got nan or inf"; 1028 | if (::fabs(yaw) == nan || ::fabs(yaw) == inf) throw "Invalid yaw angle - got nan or inf"; 1029 | if (::fabs(roll) == nan || ::fabs(roll) == inf) throw "Invalid roll angle - got nan or inf"; 1030 | // Update the entity's angles 1031 | this.DoSetAngles(pitch, yaw, roll); 1032 | } 1033 | // Overwrite SetOrigin to sanitize coordinates and allow component input 1034 | entclasses[i].DoSetOrigin <- entclasses[i].SetOrigin; 1035 | entclasses[i].SetOrigin <- function (pos, y = 0.0, z = 0.0) { 1036 | // Support input of individual components 1037 | if (typeof pos == "float" || typeof pos == "integer") { 1038 | pos = Vector(pos, y, z); 1039 | } 1040 | // Ensure the input coordinates are valid 1041 | if (::fabs(pos.x) == nan || ::fabs(pos.x) == inf) throw "Invalid X coordinate - got nan or inf"; 1042 | if (::fabs(pos.y) == nan || ::fabs(pos.y) == inf) throw "Invalid Y coordinate - got nan or inf"; 1043 | if (::fabs(pos.z) == nan || ::fabs(pos.z) == inf) throw "Invalid Z coordinate - got nan or inf"; 1044 | // Update the entity's local origin 1045 | this.DoSetOrigin(pos); 1046 | } 1047 | // Overwrite SetAbsOrigin to sanitize coordinates and allow component input 1048 | entclasses[i].DoSetAbsOrigin <- entclasses[i].SetAbsOrigin; 1049 | entclasses[i].SetAbsOrigin <- function (pos, y = 0.0, z = 0.0) { 1050 | // Support input of individual components 1051 | if (typeof pos == "float" || typeof pos == "integer") { 1052 | pos = Vector(pos, y, z); 1053 | } 1054 | // Ensure the input coordinates are valid 1055 | if (::fabs(pos.x) == nan || ::fabs(pos.x) == inf) throw "Invalid X coordinate - got nan or inf"; 1056 | if (::fabs(pos.y) == nan || ::fabs(pos.y) == inf) throw "Invalid Y coordinate - got nan or inf"; 1057 | if (::fabs(pos.z) == nan || ::fabs(pos.z) == inf) throw "Invalid Z coordinate - got nan or inf"; 1058 | // Update the entity's local origin 1059 | this.DoSetAbsOrigin(pos); 1060 | } 1061 | // Overwrite Destroy to call any destructor functions before killing 1062 | entclasses[i].DoDestroy <- entclasses[i].Destroy; 1063 | entclasses[i].Destroy <- function () { 1064 | // Check if the entity has a script scope 1065 | local scope = this.DoGetScriptScope(); 1066 | if (scope == null) return this.DoDestroy(); 1067 | // Call the script hook for the Kill input to activate destructors 1068 | if ("InputKill" in scope) scope.InputKill(); 1069 | // Proceed with destroying the entity 1070 | return this.DoDestroy(); 1071 | } 1072 | // On non-player entities, override velocity methods 1073 | if (entclasses[i] != CBasePlayer && entclasses[i] != CPortal_Player) { 1074 | // Override GetVelocity to return a promise for interpolated position 1075 | entclasses[i].GetVelocity <- function () { 1076 | // Retrieve the position on the current tick 1077 | local pos = this.GetOrigin(); 1078 | local cube = this; 1079 | // Subtract the position on the next tick 1080 | return ::ppromise(function (resolve, reject):(pos, cube) { 1081 | ppmod.wait(function ():(pos, resolve, cube) { 1082 | if (!ppmod.validate(cube)) resolve(Vector()); 1083 | resolve((cube.GetOrigin() - pos) * (1.0 / FrameTime())); 1084 | }, FrameTime()); 1085 | }); 1086 | } 1087 | // Override SetVelocity to call ppmod.push instead 1088 | entclasses[i].SetVelocity <- function (vec) { 1089 | // First, obtain the current velocity 1090 | local cube = this; 1091 | this.GetVelocity().then(function (vel):(vec, cube) { 1092 | // Then, compute the difference and use ppmod.push 1093 | return ::ppmod.push(cube, vec - vel); 1094 | }); 1095 | } 1096 | } 1097 | } catch (e) { 1098 | // Classes may fail to be modified if they've already been instantiated 1099 | // First, obtain the name of the class as a string 1100 | local classname; 1101 | switch (entclasses[i]) { 1102 | case CBaseEntity: classname = "CBaseEntity"; break; 1103 | case CBaseAnimating: classname = "CBaseAnimating"; break; 1104 | case CBaseFlex: classname = "CBaseFlex"; break; 1105 | case CBasePlayer: classname = "CBasePlayer"; break; 1106 | case CEnvEntityMaker: classname = "CEnvEntityMaker"; break; 1107 | case CLinkedPortalDoor: classname = "CLinkedPortalDoor"; break; 1108 | case CPortal_Player: classname = "CPortal_Player"; break; 1109 | case CPropLinkedPortalDoor: classname = "CPropLinkedPortalDoor"; break; 1110 | case CSceneEntity: classname = "CSceneEntity"; break; 1111 | case CTriggerCamera: classname = "CTriggerCamera"; break; 1112 | } 1113 | // Then, print a warning to the console 1114 | printl("[ppmod] Warning: failed to modify " + classname + " class: " + e); 1115 | } 1116 | } 1117 | 1118 | /****************/ 1119 | // Control flow // 1120 | /****************/ 1121 | 1122 | // Creates a logic_relay to use as a timer for calling the input script 1123 | ::ppmod.wait <- function (scr, sec, name = "") { 1124 | 1125 | // Create an optionally named logic_relay 1126 | local relay = Entities.CreateByClassname("logic_relay"); 1127 | if (name) relay.__KeyValueFromString("Targetname", name); 1128 | 1129 | // Use ppmod.addscript to attach the callback script 1130 | ppmod.addscript(relay, "OnTrigger", scr, 0, 1); 1131 | // Trigger and destroy the relay after the specified amount of seconds 1132 | EntFireByHandle(relay, "Trigger", "", sec, null, null); 1133 | relay.__KeyValueFromInt("SpawnFlags", 1); 1134 | 1135 | // Return the relay handle 1136 | return relay; 1137 | 1138 | } 1139 | 1140 | // Creates a logic_timer to use as a loop for the input script 1141 | ::ppmod.interval <- function (scr, sec = 0.0, name = "") { 1142 | 1143 | // Create an optionally named logic_timer 1144 | local timer = Entities.CreateByClassname("logic_timer"); 1145 | if (name) timer.__KeyValueFromString("Targetname", name); 1146 | 1147 | // Use ppmod.addscript to attach the callback script 1148 | ppmod.addscript(timer, "OnTimer", scr); 1149 | // Configure the timer to run on the specified interval 1150 | EntFireByHandle(timer, "RefireTime", sec.tostring(), 0.0, null, null); 1151 | EntFireByHandle(timer, "Enable", "", 0.0, null, null); 1152 | 1153 | // Return the timer handle 1154 | return timer; 1155 | 1156 | } 1157 | 1158 | // Time the execution of the input script using console ticks 1159 | ::ppmod.ontick <- function (scr, pause = true, timeout = -1) { 1160 | 1161 | // If the input is a string, compile it into a function 1162 | if (typeof scr == "string") scr = compilestring(scr); 1163 | // Validate the input script argument 1164 | if (typeof scr != "function") throw "ontick: Invalid script argument"; 1165 | 1166 | // Add the input to the script queue 1167 | if (timeout == -1) scr = "ppmod.scrq_get(" + ppmod.scrq_add(scr, -1) + ")()"; 1168 | else scr = "ppmod.scrq_get(" + ppmod.scrq_add(scr, 1) + ")()"; 1169 | 1170 | // If the game is paused and pause == true, recurse on the next tick and exit 1171 | if (pause && FrameTime() == 0.0) { 1172 | SendToConsole("script ppmod.ontick(\"" + scr + "\", true, " + timeout + ")"); 1173 | return; 1174 | } 1175 | 1176 | // A timeout of -1 indicates that the script should run on every tick, indefinitely 1177 | if (timeout == -1) { 1178 | SendToConsole("script " + scr + ";script ppmod.ontick(\"" + scr + "\", " + pause + ")"); 1179 | return; 1180 | } 1181 | // If timeout has reached 0, call the attached script and exit 1182 | if (timeout == 0) return SendToConsole("script " + scr); 1183 | // Otherwise, recurse on the next tick with a decremented timeout 1184 | SendToConsole("script ppmod.ontick(\"" + scr + "\", " + pause + ", " + (timeout - 1) + ")"); 1185 | 1186 | } 1187 | 1188 | // Runs the input script on map start or save load 1189 | ::ppmod.onauto <- function (scr, onload = false) { 1190 | 1191 | // Create a logic_auto for listening to events on which to run the script 1192 | local auto = Entities.CreateByClassname("logic_auto"); 1193 | 1194 | // In online multiplayer games, we delay spawning until both players are ready 1195 | if (IsMultiplayer()) scr = function ():(scr) { 1196 | 1197 | // Create a table to allow for accessing the interval from within itself 1198 | local ref = { interval = null }; 1199 | 1200 | // Set up an interval to wait for blue (the host) to spawn 1201 | ref.interval = ppmod.interval(function ():(scr, ref) { 1202 | 1203 | // Find the host player using their special keyword 1204 | local blue = Entities.FindByName(null, "!player_blue"); 1205 | // Fall back to the first player handle if blue wasn't found 1206 | if (!blue || !blue.IsValid() || blue.GetClassname() != "player") { 1207 | blue = Entities.FindByClassname(null, "player"); 1208 | } 1209 | // If no host player was found, continue 1210 | if (!blue || !blue.IsValid()) return; 1211 | 1212 | // Host was found, stop the interval 1213 | ref.interval.Destroy(); 1214 | 1215 | // If on split-screen, we're done, run the script 1216 | if (IsLocalSplitScreen()) { 1217 | if (typeof scr == "string") return compilestring(scr)(); 1218 | // Wait for players to re-teleport 1219 | return ppmod.wait(scr, 1.5); 1220 | } 1221 | 1222 | // Find the lowest significant point of the world's bounding box estimate 1223 | local ent = null, lowest = 0, curr; 1224 | while (ent = Entities.Next(ent)) { 1225 | // Skip invalid handles 1226 | if (!ent.IsValid()) continue; 1227 | // Keep track of the lowest point in the map 1228 | curr = ent.GetOrigin().z + ent.GetBoundingMins().z; 1229 | if (curr < lowest) lowest = curr; 1230 | } 1231 | // Additional decrement just to make sure we're below anything significant 1232 | lowest -= 1024.0; 1233 | 1234 | // We move the host below the map and wait until they are teleported back up 1235 | // This happens once both players finish connecting in networked games 1236 | blue.SetOrigin(Vector(0, 0, lowest)); 1237 | 1238 | // Set up an interval to wait for orange (the second player) to spawn 1239 | ref.interval = ppmod.interval(function ():(blue, lowest, scr, ref) { 1240 | 1241 | // Find the second player using their special keyword 1242 | local red = Entities.FindByClassname(null, "!player_orange"); 1243 | // Fall back to the player handle after the host's if orange wasn't found 1244 | if (!red || !red.IsValid() || red.GetClassname() != "player") { 1245 | red = Entities.FindByClassname(blue, "player"); 1246 | } 1247 | // If red was not found, or blue is still under the map, continue 1248 | if (!red || !red.IsValid() || blue.GetOrigin().z <= lowest) return; 1249 | 1250 | // Run the input script 1251 | if (typeof scr == "string") compilestring(scr)(); 1252 | else scr(); 1253 | // Red was found, stop the interval 1254 | ref.interval.Destroy(); 1255 | 1256 | }); 1257 | 1258 | }); 1259 | 1260 | }; 1261 | 1262 | // Attach the script to map start events 1263 | ppmod.addscript(auto, "OnNewGame", scr); 1264 | ppmod.addscript(auto, "OnMapTransition", scr); 1265 | // Optionally, attach to save load events 1266 | if (onload) ppmod.addscript(auto, "OnLoadGame", scr); 1267 | // Return the logic_auto 1268 | return auto; 1269 | 1270 | } 1271 | 1272 | // Pauses the game until the specified ppromise resolves 1273 | ::ppmod.preload <- function (promise):(ppromise_methods) { 1274 | 1275 | // Validate the promise 1276 | if (promise.then != ppromise_methods.then) throw "preload: Invalid promise argument"; 1277 | 1278 | // Run inside a ppromise to allow for awaiting preload completion 1279 | return ppromise(function (resolve, reject):(promise) { 1280 | 1281 | // Pause the game 1282 | SendToConsole("setpause"); 1283 | 1284 | local scrq_idx = ppmod.scrq_add(function ():(promise, resolve) { 1285 | // Unpause the game once the promise resolves 1286 | promise.then(function (_):(resolve) { 1287 | SendToConsole("unpause"); 1288 | resolve(); 1289 | }); 1290 | }, 1); 1291 | // Run the promise as a command to ensure it's in sync with the pause 1292 | SendToConsole("script ppmod.scrq_get("+ scrq_idx +")()"); 1293 | 1294 | }); 1295 | 1296 | }; 1297 | 1298 | // Works around script timeouts by catching the exception they throw 1299 | ::ppmod.detach <- function (scr, args, stack = null) { 1300 | 1301 | // Validate the callback argument 1302 | if (typeof scr != "function") throw "detach: Invalid callback argument"; 1303 | // Retrieve a stack trace to the line on which ppmod.detach was called 1304 | if (stack == null) stack = getstackinfos(2); 1305 | 1306 | // Run the input function in a try/catch block 1307 | try { return scr(args) } 1308 | catch (e) { 1309 | 1310 | // If the exception is caused by SQQuerySuspend, recurse 1311 | if (e.find("Script terminated by SQQuerySuspend") != null) { 1312 | return ppmod.detach(scr, args, stack); 1313 | } 1314 | // Otherwise, mimic error output using the stack trace 1315 | printl("\nAN ERROR HAS OCCURED [" + e + "]"); 1316 | printl("Caught within ppmod.detach in file " + stack.src + " on line " + stack.line + "\n"); 1317 | 1318 | } 1319 | 1320 | } 1321 | 1322 | /********************/ 1323 | // Player interface // 1324 | /********************/ 1325 | 1326 | // Provides more information about and ways to interact with a player 1327 | ::ppmod.player <- class { 1328 | 1329 | // Holds the player entity 1330 | ent = null; 1331 | 1332 | // Entities used for managing player state 1333 | eyes = null; 1334 | gameui = null; 1335 | proxy = null; 1336 | 1337 | // Internal values 1338 | groundstate = false; 1339 | velprev = 0; 1340 | gravtrig = null; 1341 | landscript = []; 1342 | initinterval = null; 1343 | fricfactor = 4.0; 1344 | 1345 | /** 1346 | * The properties of logic_measure_movement seem to search for entities by 1347 | * targetname exclusively. This function sets the player's name to a unique 1348 | * string for just long enough to update MeasureTarget, and then sets it 1349 | * back to what it was right away. 1350 | */ 1351 | static target_eyes = function () { 1352 | 1353 | // Store the current player name and generate a unique temporary name 1354 | local oldname = this.ent.GetName(); 1355 | local newname = "this_ent_" + Time() + UniqueString(); 1356 | 1357 | /** 1358 | * Push these inputs to the entity I/O queue back to back, one by one. 1359 | * This ensures that we're changing the name for only as long as is 1360 | * necessary, and doesn't let any other inputs get in between these. 1361 | */ 1362 | EntFireByHandle(this.ent, "AddOutput", "Targetname " + newname, 0.0, null, null); 1363 | EntFireByHandle(this.eyes, "SetMeasureTarget", newname, 0.0, null, null); 1364 | // Use the script queue to reset the player's targetname 1365 | // This retains full accuracy, as to not drop any special characters 1366 | local scrqidx = ppmod.scrq_add(function (self):(oldname) { self.__KeyValueFromString("Targetname", oldname) }, 1); 1367 | EntFireByHandle(this.ent, "RunScriptCode", "ppmod.scrq_get("+ scrqidx +")(self)", 0.0, null, null); 1368 | 1369 | }; 1370 | 1371 | constructor (player) { 1372 | 1373 | // Validate the input entity handle 1374 | if (!ppmod.validate(player)) throw "player: Invalid entity handle"; 1375 | if (!(player instanceof CBasePlayer)) throw "player: Entity is not a player"; 1376 | // Keep track of the constructing player handle 1377 | this.ent = player; 1378 | 1379 | // Create a game_ui for listening to player movement inputs 1380 | this.gameui = Entities.CreateByClassname("game_ui"); 1381 | // Set up and (but don't yet activate) the game_ui entity 1382 | this.gameui.__KeyValueFromInt("FieldOfView", -1); 1383 | 1384 | // One logic_playerproxy is required for registering jumping and ducking 1385 | // This breaks if more than one is created, so we use an existing one if available 1386 | this.proxy = Entities.FindByClassname(null, "logic_playerproxy"); 1387 | if (!this.proxy) this.proxy = Entities.CreateByClassname("logic_playerproxy"); 1388 | 1389 | // Create a logic_measure_movement for getting player eye angles 1390 | this.eyes = Entities.CreateByClassname("logic_measure_movement"); 1391 | // Generate a unique name for the entity 1392 | local eyename = "pplayer_eyes_" + Time() + UniqueString(); 1393 | // Set MeasureType to measure eye position 1394 | this.eyes.__KeyValueFromInt("MeasureType", 1); 1395 | // Point the entity back at itself 1396 | this.eyes.__KeyValueFromString("Targetname", eyename); 1397 | this.eyes.__KeyValueFromString("TargetReference", eyename); 1398 | this.eyes.__KeyValueFromString("Target", eyename); 1399 | // The MeasureReference doesn't update unless set with the input 1400 | EntFireByHandle(this.eyes, "SetMeasureReference", eyename, 0.0, null, null); 1401 | // Update the MeasureTarget 1402 | local target_eyes_this = target_eyes.bindenv(this); 1403 | target_eyes_this(); 1404 | // The MeasureTarget must be updated on each game load 1405 | local auto = Entities.CreateByClassname("logic_auto"); 1406 | auto.__KeyValueFromString("OnMapSpawn", "!self\x001BRunScriptCode\x001Bppmod.scrq_get(" + ppmod.scrq_add(target_eyes_this, -1) + ")()\x001B0\x001B-1"); 1407 | // Enable the logic_measure_movement entity 1408 | EntFireByHandle(this.eyes, "Enable", "", 0.0, null, null); 1409 | // Set the roll angle of this.eyes to a silly value 1410 | // This lets us later wait for the entity to be fully initialized 1411 | this.eyes.SetAngles(0.0, 0.0, 370.0); 1412 | 1413 | // Some routines have to be performed in a tick loop 1414 | ppmod.interval((function () { 1415 | 1416 | /****************************************** 1417 | * Monitor whether the player is grounded * 1418 | ******************************************/ 1419 | 1420 | // Get the player's velocity along the Z axis 1421 | local velZ = this.ent.GetVelocity().z; 1422 | 1423 | // If the velocity has been non-zero for two ticks, consider the player ungrounded 1424 | if (this.velprev != 0.0 && velZ != 0.0) this.groundstate = false; 1425 | // If the player was just moving down and has now stopped, consider them grounded 1426 | else if (this.velprev <= 0.0 && velZ == 0.0 && !this.groundstate) { 1427 | this.groundstate = true; 1428 | // Run each attached landing handler 1429 | for (local i = 0; i < this.landscript.len(); i ++) this.landscript[i](); 1430 | } 1431 | 1432 | // Update the velocity of the previous tick 1433 | this.velprev = velZ; 1434 | 1435 | /*************************************** 1436 | * Update the gravity trigger position * 1437 | ***************************************/ 1438 | 1439 | if (this.gravtrig) { 1440 | // If simply parented, the trigger won't have any effect 1441 | this.gravtrig.SetAbsOrigin(this.ent.GetCenter()); 1442 | } 1443 | 1444 | /********************************************************** 1445 | * Recalculate the player's friction for the current tick * 1446 | **********************************************************/ 1447 | 1448 | // Only needed if grounded and friction is not default 1449 | if (this.groundstate && this.fricfactor != 4.0) { 1450 | 1451 | // These calculations are time-dependant, obtain the frame time 1452 | local ftime = FrameTime(); 1453 | // Obtain the player's velocity, its normal vector and amplitude 1454 | local vel = this.ent.GetVelocity(); 1455 | local veldir = vel + Vector(); 1456 | local absvel = veldir.Norm(); 1457 | 1458 | // Cancel out existing friction calculations 1459 | if (absvel >= 100.0) { 1460 | vel *= 1.0 / (1.0 - ftime * 4.0); 1461 | } else { 1462 | vel += veldir * (ftime * 400.0); 1463 | } 1464 | 1465 | // Simulate our own friction 1466 | if (absvel >= 100.0) { 1467 | vel *= 1.0 - ftime * this.fricfactor; 1468 | } else if (this.fricfactor > 0.0) { 1469 | if (this.fricfactor / 0.6 < absvel) { 1470 | vel -= veldir * (ftime * 400.0); 1471 | } else if (absvel != 0.0) { 1472 | vel.x = 0.0; 1473 | vel.y = 0.0; 1474 | } 1475 | } 1476 | 1477 | // Apply calculated velocity 1478 | this.ent.SetVelocity(vel); 1479 | 1480 | } 1481 | 1482 | }).bindenv(this)); 1483 | 1484 | // Set up a trigger_gravity for modifying the player's local gravity 1485 | ppmod.trigger(this.ent.GetOrigin() + Vector(0, 0, 36.5), Vector(16, 16, 36), "trigger_gravity", Vector(), true).then((function (trigger) { 1486 | // Disable the trigger by default 1487 | trigger.__KeyValueFromFloat("Gravity", 1.0); 1488 | EntFireByHandle(trigger, "Disable", "", 0.0, null, null); 1489 | // Store the trigger for later use and verification 1490 | this.gravtrig = trigger; 1491 | }).bindenv(this)); 1492 | 1493 | } 1494 | 1495 | /** 1496 | * Resolves a ppromise once eyes returns a valid roll angle and once a 1497 | * trigger_gravity has been created. These are the only asynchronous 1498 | * operations, hence why we're checking for these in particular. 1499 | */ 1500 | function init () { 1501 | return ppromise((function (resolve, reject) { 1502 | this.initinterval = ppmod.interval((function ():(resolve) { 1503 | // Check for proper setup of eyes and gravtrig 1504 | if (this.eyes.GetAngles().z == 370.0) return; 1505 | if (!this.gravtrig) return; 1506 | // Stop the interval and resolve with the ppmod.player instance 1507 | this.initinterval.Destroy(); 1508 | resolve(this); 1509 | }).bindenv(this)); 1510 | }).bindenv(this)); 1511 | } 1512 | 1513 | // Checks if the player is holding a physics prop 1514 | function holding () { 1515 | 1516 | /** 1517 | * When a player picks up a prop, a player_pickup entity is created 1518 | * and attached to the player. If we can find such an entity, that 1519 | * means the player is holding something. 1520 | */ 1521 | local curr = null; 1522 | while (curr = Entities.FindByClassname(curr, "player_pickup")) { 1523 | if (curr.GetMoveParent() == this.ent) return true; 1524 | } 1525 | return false; 1526 | 1527 | }; 1528 | 1529 | // Attaches a function to the event of the player using the jump input 1530 | function onjump (scr) { 1531 | local scrqstr = "ppmod.scrq_get(" + ppmod.scrq_add(scr) + ")()"; 1532 | ppmod.addoutput(this.proxy, "OnJump", this.ent, "RunScriptCode", "if(self==activator)" + scrqstr); 1533 | } 1534 | 1535 | // Attaches a function to the event of the player landing on solid ground 1536 | function onland (scr) { 1537 | // Validate the input script argument 1538 | if (typeof scr == "string") scr = compilestring(scr); 1539 | if (typeof scr != "function") throw "onland: Invalid script argument"; 1540 | // Push the script to the array of landing handlers 1541 | this.landscript.push(scr); 1542 | } 1543 | 1544 | // Attaches a function to the event of the player finishing the crouching animation 1545 | function onduck (scr) { 1546 | local scrqstr = "ppmod.scrq_get(" + ppmod.scrq_add(scr) + ")()"; 1547 | ppmod.addoutput(this.proxy, "OnDuck", this.ent, "RunScriptCode", "if(self==activator)" + scrqstr); 1548 | } 1549 | 1550 | // Attaches a function to the event of the player finishing the uncrouching animation 1551 | function onunduck (scr) { 1552 | local scrqstr = "ppmod.scrq_get(" + ppmod.scrq_add(scr) + ")()"; 1553 | ppmod.addoutput(this.proxy, "OnUnDuck", this.ent, "RunScriptCode", "if(self==activator)" + scrqstr); 1554 | } 1555 | 1556 | // Returns true if the player is in the process of ducking/unducking, false otherwise 1557 | function ducking () return this.ent.EyePosition().z - this.ent.GetOrigin().z < 63.999; 1558 | // Returns true if the player is on the ground, false otherwise 1559 | function grounded () return this.ent.groundstate; 1560 | 1561 | // Attaches a function to the event of the player giving a certain action input 1562 | function oninput (str, scr) { 1563 | if (typeof str != "string") throw "oninput: Invalid command string argument"; 1564 | if (str[0] == '+') str = "pressed" + str.slice(1); 1565 | else str = "unpressed" + str.slice(1); 1566 | ppmod.addscript(this.gameui, str, scr); 1567 | // Activate the entity only once an output has been added 1568 | // This prevents prediction from being unnecessarily turned off in co-op 1569 | EntFireByHandle(this.gameui, "Activate", "", 0.0, this.ent, null); 1570 | }; 1571 | 1572 | // Sets the player's gravity scale to the given value 1573 | function gravity (factor) { 1574 | // Ensure the gravity trigger exists 1575 | if (!ppmod.validate(this.gravtrig)) throw "gravity: No valid gravity trigger"; 1576 | // Disable the trigger if factor is 1.0 (default), enable otherwise 1577 | if (factor == 1.0) EntFireByHandle(this.gravtrig, "Disable", "", 0.0, null, null); 1578 | else EntFireByHandle(this.gravtrig, "Enable", "", 0.0, null, null); 1579 | // Zero values have no effect, this is hacky but works well enough 1580 | if (factor == 0.0) this.gravtrig.__KeyValueFromString("Gravity", "0.0000000000000001"); 1581 | else this.gravtrig.__KeyValueFromFloat("Gravity", factor); 1582 | }; 1583 | 1584 | // Sets the player's friction to the given value 1585 | function friction (fric) return this.fricfactor = fric; 1586 | 1587 | // Simulates player movement for one time step using Source engine movement physics 1588 | function movesim (move, accel = 10.0, fric = 0.0, sfric = 0.25, grav = null, ftime = null, eyes = null, grounded = null) { 1589 | 1590 | // Set default values for unset parameters 1591 | if (grav == null) grav = Vector(0, 0, -600); 1592 | if (ftime == null) ftime = FrameTime(); 1593 | if (eyes == null) eyes = this.eyes; 1594 | if (grounded == null) grounded = this.grounded(); 1595 | 1596 | // If in the air, scale down all acceleration by the "surface friction" parameter 1597 | if (!grounded) accel *= sfric; 1598 | 1599 | // Obtain the player velocity in full form and along just the X/Y axis 1600 | local vel = this.ent.GetVelocity(); 1601 | local horizvel = Vector(vel.x, vel.y); 1602 | 1603 | // If necessary, calculate friction 1604 | if (fric != 0.0 && grounded) { 1605 | // Obtain the normal vector and amplitude of the player's horizontal velocity 1606 | // This avoids issues when grounded == true but the player isn't actually grounded 1607 | local veldir = horizvel + Vector(); 1608 | local absvel = veldir.Norm(); 1609 | // Calculate friction for this time step 1610 | if (absvel >= 100.0) { 1611 | vel *= 1.0 - ftime * fric; 1612 | } else if (fric / 0.6 < absvel) { 1613 | vel -= veldir * (ftime * 400.0); 1614 | } else if (absvel != 0.0) { 1615 | vel.x = 0.0; 1616 | vel.y = 0.0; 1617 | } 1618 | } 1619 | 1620 | // Obtain the forward and left vectors, with the Z axis removed 1621 | local forward = eyes.GetForwardVector().Normalize2D(); 1622 | local left = eyes.GetLeftVector().Normalize2D(); 1623 | 1624 | // Calculate the direction and speed in which the player "wishes" to move 1625 | local wishvel = Vector(); 1626 | wishvel.x = forward.x * move.y + left.x * move.x; 1627 | wishvel.y = forward.y * move.y + left.y * move.x; 1628 | local wishspeed = wishvel.Norm(); 1629 | 1630 | // Calculate how much to accelerate the player by 1631 | local currspeed = horizvel.Dot(wishvel); 1632 | local addspeed = wishspeed - currspeed; 1633 | local accelspeed = accel * ftime * wishspeed; 1634 | if (accelspeed > addspeed) accelspeed = addspeed; 1635 | 1636 | // Calculate and apply the final player velocity 1637 | this.ent.SetVelocity(vel + wishvel * accelspeed + grav * ftime); 1638 | 1639 | } 1640 | 1641 | } 1642 | 1643 | // Constructor for the ppmod.portal prototypal class 1644 | // Provides utilities for working with portals 1645 | ::ppmod.portal <- function (portal) { 1646 | 1647 | // Most properties are stored in the portal entity's script scope 1648 | if (!portal.ValidateScriptScope()) throw "portal: Could not validate script scope"; 1649 | local scope = portal.GetScriptScope(); 1650 | // If an instance already exists in the script scope, return that 1651 | if ("ppmod_portal" in scope) return scope.ppmod_portal; 1652 | // Otherwise, create a blank table for the prototypal instance 1653 | scope.ppmod_portal <- {}; 1654 | 1655 | // Create a trigger for detecting collisions with the portal 1656 | local trigger = Entities.CreateByClassname("trigger_multiple"); 1657 | 1658 | // Position and scale the trigger to submerge the portal 1659 | trigger.SetAbsOrigin(portal.GetOrigin()); 1660 | trigger.SetForwardVector(portal.GetForwardVector()); 1661 | trigger.SetSize(Vector(-8, -32, -56), Vector(0, 32, 56)); 1662 | // Parent the trigger to the portal 1663 | EntFireByHandle(trigger, "SetParent", "!activator", 0.0, portal, null); 1664 | // Make the trigger non-solid and activated by clients, NPCs, and props 1665 | trigger.__KeyValueFromInt("Solid", 3); 1666 | trigger.__KeyValueFromInt("CollisionGroup", 10); 1667 | trigger.__KeyValueFromInt("SpawnFlags", 11); 1668 | // Enable the trigger 1669 | EntFireByHandle(trigger, "Enable", "", 0.0, null, null); 1670 | 1671 | // Keeps track of when the last teleport occurred 1672 | scope.ppmod_portal.tptime <- 0.0; 1673 | // Stores all attached OnTeleport functions 1674 | scope.ppmod_portal.tpfunc <- []; 1675 | 1676 | // Manages trigger OnEndTouch events (something leaving the trigger volume) 1677 | local scrq_idx = ppmod.scrq_add(function (ent):(scope) { 1678 | // Using runscript lets us push this to the end of the entity I/O queue 1679 | ppmod.runscript("worldspawn", function ():(ent, scope) { 1680 | 1681 | /** 1682 | * Whenever an entity teleports through a portal, the 1683 | * OnEntityTeleportFromMe output updates tptime with the current 1684 | * server time. We can compare this to when the trigger fires 1685 | * OnEndTouch, and if they're the same, we must be looking at the 1686 | * same entity. This lets us retrieve it as the activator. 1687 | */ 1688 | local ticks_now = (Time() / FrameTime()).tointeger(); 1689 | local ticks_tp = (scope.ppmod_portal.tptime / FrameTime()).tointeger(); 1690 | 1691 | // Check if the two time reports match 1692 | // Currently allows for a 1 tick tolerance, ideally 0 one day 1693 | if (ticks_now - ticks_tp > 1) return; 1694 | 1695 | // If it did, something must've teleported - call attached functions 1696 | for (local i = 0; i < scope.ppmod_portal.tpfunc.len(); i ++) { 1697 | scope.ppmod_portal.tpfunc[i](ent); 1698 | } 1699 | 1700 | }); 1701 | }, -1); 1702 | 1703 | // Attach OnEndTouch and OnEntityTeleportFromMe outputs to the trigger and portal, respectively 1704 | trigger.__KeyValueFromString("OnEndTouch", "worldspawn\x001BRunScriptCode\x001Bppmod.scrq_get(" + scrq_idx + ")(activator)\x001B0\x001B-1"); 1705 | portal.__KeyValueFromString("OnEntityTeleportFromMe", "!self\x001BRunScriptCode\x001Bself.GetScriptScope().ppmod_portal.tptime<-Time()\x001B0\x001B-1"); 1706 | 1707 | // Attaches a function to the event of a portal teleporting something 1708 | scope.ppmod_portal.OnTeleport <- function (func):(scope) { 1709 | scope.ppmod_portal.tpfunc.push(func); 1710 | }; 1711 | 1712 | // Internal utility function - sets up a new func_portal_detector 1713 | local new_detector = function (allids):(portal) { 1714 | 1715 | // Create the func_portal_detector entity 1716 | local detector = Entities.CreateByClassname("func_portal_detector"); 1717 | 1718 | // Place it at the portal's origin with a minimal bounding box 1719 | detector.__KeyValueFromInt("Solid", 3); 1720 | detector.__KeyValueFromInt("CollisionGroup", 10); 1721 | detector.SetAbsOrigin(portal.GetOrigin()); 1722 | detector.SetSize(Vector(-0.1, -0.1, -0.1), Vector(0.1, 0.1, 0.1)); 1723 | // Whether to match for all portal linkage IDs 1724 | detector.__KeyValueFromInt("CheckAllIDs", allids); 1725 | 1726 | // Enable and return the detector entity 1727 | EntFireByHandle(detector, "Enable", "", 0.0, null, null); 1728 | return detector; 1729 | 1730 | }; 1731 | 1732 | // Returns a ppromise that resolves to the portal's color index 1733 | scope.ppmod_portal.GetColor <- function ():(new_detector) { 1734 | return ppromise(function (resolve, reject):(new_detector) { 1735 | // Add the resolve callback to the script queue 1736 | local scrq_idx = ppmod.scrq_add(resolve, 1); 1737 | // Create a detector and listen for its OnStartTouchPortalX inputs 1738 | local detector = new_detector(1); 1739 | detector.__KeyValueFromString("OnStartTouchPortal1", "!self\x001BRunScriptCode\x001Bppmod.scrq_get(" + scrq_idx + ")(1);self.Destroy()\x001B0\x001B1"); 1740 | detector.__KeyValueFromString("OnStartTouchPortal2", "!self\x001BRunScriptCode\x001Bppmod.scrq_get(" + scrq_idx + ")(2);self.Destroy()\x001B0\x001B1"); 1741 | }); 1742 | }; 1743 | 1744 | // Returns a ppromise that resolves to true if the portal is active, false otherwise 1745 | scope.ppmod_portal.GetActivatedState <- function ():(new_detector) { 1746 | return ppromise(function (resolve, reject):(new_detector) { 1747 | // Add the resolve callback to the script queue 1748 | local scrq_idx = ppmod.scrq_add(resolve, 1); 1749 | // Create a detector and listen for its OnStartTouchLinkedPortal output 1750 | local detector = new_detector(1); 1751 | detector.__KeyValueFromString("OnStartTouchLinkedPortal", "!self\x001BRunScriptCode\x001Bppmod.scrq_get(" + scrq_idx + ")(true);self.Destroy()\x001B0\x001B1"); 1752 | // Connect OnUser1 to resolve(false) 1753 | detector.__KeyValueFromString("OnUser1", "!self\x001BRunScriptCode\x001Bif(self.IsValid())ppmod.scrq_get(" + scrq_idx + ")(false)\x001B0\x001B1"); 1754 | detector.__KeyValueFromString("OnUser1", "!self\x001BKill\x001B\x001B0\x001B1"); 1755 | // Call FireUser1, which sets up a sort of race condition 1756 | // If OnStartTouchLinkedPortal gets there first, this won't do anything 1757 | EntFireByHandle(detector, "FireUser1", "", 0.0, null, null); 1758 | }); 1759 | }; 1760 | 1761 | // Returns a ppromise that resolves to the linkage group ID of the portal 1762 | scope.ppmod_portal.GetLinkageGroupID <- function ():(new_detector) { 1763 | return ppromise(function (resolve, reject):(new_detector) { 1764 | 1765 | // Create a detector that activates only for a specific linkage group 1766 | local detector = new_detector(0); 1767 | // Keep track of the currently observed linkage group 1768 | local params = { id = 0 }; 1769 | 1770 | // Checks whether the portal is of the currently observed linkage group 1771 | local check = function ():(detector, params) { 1772 | // If the detector has been deleted, we're done 1773 | if (!detector.IsValid()) return; 1774 | // Update the detector's target linkage group ID 1775 | detector.__KeyValueFromInt("LinkageGroupID", ++params.id); 1776 | // Update the detector's position and re-enable it to get outputs to refire 1777 | detector.SetAbsOrigin(detector.GetOrigin()); 1778 | EntFireByHandle(detector, "Enable", "", 0.0, null, null); 1779 | // Call FireUser1 to recurse this check 1780 | EntFireByHandle(detector, "FireUser1", "", 0.0, null, null); 1781 | }; 1782 | 1783 | // Store all relevant parameters in the script queue 1784 | local scrq_idx_resolve = ppmod.scrq_add(resolve, 1); 1785 | local scrq_idx_params = ppmod.scrq_add(params, 1); 1786 | local scrq_idx_check = ppmod.scrq_add(check, -1); 1787 | 1788 | /** 1789 | * If the detector outputs OnStartTouchPortal, we resolve with the 1790 | * currently observed linkage ID, clean up the script queue, and kill 1791 | * the detector. Otherwise, if OnUser1 is outputted first, we 1792 | * continue iterating until the right linkage ID is found. 1793 | */ 1794 | detector.__KeyValueFromString("OnStartTouchPortal", "!self\x001BRunScriptCode\x001Bppmod.scrq_get(" + scrq_idx_resolve + ")(ppmod.scrq_get(" + scrq_idx_params + ").id);ppmod.scrq[" + scrq_idx_check + "] = null;self.Destroy()\x001B0\x001B1"); 1795 | detector.__KeyValueFromString("OnUser1", "!self\x001BRunScriptCode\x001Bif(self.IsValid())ppmod.scrq_get(" + scrq_idx_check + ")()\x001B0\x001B-1"); 1796 | 1797 | // Call FireUser1 to start iterating through linkage IDs 1798 | EntFireByHandle(detector, "FireUser1", "", 0.0, null, null); 1799 | 1800 | }); 1801 | }; 1802 | 1803 | // Returns a ppromise that resolves to a handle of this portal's active linked partner 1804 | scope.ppmod_portal.GetPartnerInstance <- function ():(portal, scope) { 1805 | return ppromise(function (resolve, reject):(portal, scope) { 1806 | // First, obtain the linkage group ID of this portal 1807 | scope.ppmod_portal.GetLinkageGroupID().then(function (id):(resolve, portal) { 1808 | 1809 | // Create a recursive function for finding the other portal 1810 | local param = { next = null }; 1811 | param.next = function (curr):(id, resolve, portal, param) { 1812 | 1813 | // Get the handle of the next portal 1814 | curr = Entities.FindByClassname(curr, "prop_portal"); 1815 | // If we've wrapped around to null, no partner was found 1816 | if (curr == null) return resolve(null); 1817 | // If we've encountered the same portal we started with, continue 1818 | if (curr == portal) return param.next(curr); 1819 | 1820 | // Obtain a ppmod.portal instance of the current portal 1821 | local pportal = ppmod.portal(curr); 1822 | // Obtain the linkage group ID of the current portal 1823 | pportal.GetLinkageGroupID().then(function (currid):(resolve, param, curr, pportal, id) { 1824 | 1825 | // If the linkage IDs do not match, continue 1826 | if (currid != id) return param.next(curr); 1827 | 1828 | // If the current portal is active, we've found it. Otherwise, continue. 1829 | pportal.GetActivatedState().then(function (state):(resolve, param, curr) { 1830 | if (state) return resolve(curr); 1831 | return param.next(curr); 1832 | }); 1833 | 1834 | }); 1835 | 1836 | }; 1837 | // Start the recursion 1838 | param.next(null); 1839 | 1840 | }); 1841 | }); 1842 | }; 1843 | 1844 | // Return the ppmod.portal prototypal class instance 1845 | return scope.ppmod_portal; 1846 | 1847 | } 1848 | 1849 | // Stores all attached ppmod.onportal callback functions 1850 | local onportalfunc = []; 1851 | // Attaches a function to be called on every portal shot 1852 | ::ppmod.onportal <- function (scr):(onportalfunc) { 1853 | 1854 | // If the input is a string, compile it into a function 1855 | if (typeof scr == "string") scr = compilestring(scr); 1856 | // Validate the input script argument 1857 | if (typeof scr != "function") throw "onportal: Invalid script argument"; 1858 | 1859 | // Push the function to the attached function array 1860 | onportalfunc.push(scr); 1861 | 1862 | // Return if the setup has already been run before 1863 | if (onportalfunc.len() != 1) return; 1864 | 1865 | // Handles portal OnPlacedSuccessfully outputs 1866 | local scrq_idx = ppmod.scrq_add(function (portal, first):(onportalfunc) { 1867 | // Using runscript lets us push this to the end of the entity I/O queue 1868 | ppmod.runscript("worldspawn", function ():(portal, first, onportalfunc) { 1869 | 1870 | local pgun = null; 1871 | local color = null; 1872 | 1873 | // Iterate through all weapon_portalgun entities 1874 | while (pgun = Entities.FindByClassname(pgun, "weapon_portalgun")) { 1875 | 1876 | // Validate the entity and its script scope 1877 | if (!pgun.IsValid()) continue; 1878 | if (!pgun.ValidateScriptScope()) continue; 1879 | // Retrieve the script scope 1880 | local scope = pgun.GetScriptScope(); 1881 | 1882 | /** 1883 | * Determine the color of the portal by finding a portalgun which 1884 | * fired one of its two portals at the same time as this check was 1885 | * called. The input which matches the time marks the color index. 1886 | */ 1887 | if (scope.ppmod_onportal_time1 == Time()) { 1888 | color = 1; 1889 | break; 1890 | } 1891 | if (scope.ppmod_onportal_time2 == Time()) { 1892 | color = 2; 1893 | break; 1894 | } 1895 | 1896 | } 1897 | 1898 | // Construct a table with information about the portal placement 1899 | local info = { 1900 | portal = portal, // Portal entity handle 1901 | weapon = pgun, // Portal gun handle (null if none) 1902 | color = color, // Portal color index (1 or 2) 1903 | first = first // Whether this is the first appearance of the portal 1904 | }; 1905 | 1906 | // Call each attached function, passing the table constructed above 1907 | for (local i = 0; i < onportalfunc.len(); i ++) { 1908 | onportalfunc[i](info); 1909 | } 1910 | 1911 | }); 1912 | }, -1); 1913 | 1914 | // Check for new portals and portalguns on an interval 1915 | ppmod.interval(function ():(scrq_idx) { 1916 | 1917 | // Entity iterator 1918 | local curr = null; 1919 | 1920 | // Iterate through all new weapon_portalgun entities 1921 | while (curr = Entities.FindByClassname(curr, "weapon_portalgun")) { 1922 | // Validate the entity and its script scope 1923 | if (!curr.IsValid()) continue; 1924 | if (!curr.ValidateScriptScope()) continue; 1925 | 1926 | // Retrieve the script scope, continue if setup already performed 1927 | local scope = curr.GetScriptScope(); 1928 | if ("ppmod_onportal_time1" in scope) continue; 1929 | 1930 | // Keep track of the time when each portal is fired 1931 | scope.ppmod_onportal_time1 <- 0.0; 1932 | scope.ppmod_onportal_time2 <- 0.0; 1933 | 1934 | // Attach the OnFiredPortalX functions for updating the time variables 1935 | curr.__KeyValueFromString("OnFiredPortal1", "!self\x001BRunScriptCode\x001Bself.GetScriptScope().ppmod_onportal_time1<-Time()\x001B0\x001B-1"); 1936 | curr.__KeyValueFromString("OnFiredPortal2", "!self\x001BRunScriptCode\x001Bself.GetScriptScope().ppmod_onportal_time2<-Time()\x001B0\x001B-1"); 1937 | 1938 | } 1939 | 1940 | // Iterate through all new prop_portal entities 1941 | while (curr = Entities.FindByClassname(curr, "prop_portal")) { 1942 | // Validate the entity and its script scope 1943 | if (!curr.IsValid()) continue; 1944 | if (!curr.ValidateScriptScope()) continue; 1945 | 1946 | // Retrieve the script scope, continue if setup already performed 1947 | local scope = curr.GetScriptScope(); 1948 | if ("ppmod_onportal_flag" in scope) continue; 1949 | 1950 | // Call the check function each time this portal is placed 1951 | curr.__KeyValueFromString("OnPlacedSuccessfully", "!self\x001BRunScriptCode\x001Bppmod.scrq_get("+ scrq_idx +")(self,false)\x001B0\x001B-1"); 1952 | // Call the check function now, indicating that this is the first encounter 1953 | ppmod.scrq_get(scrq_idx)(curr, true); 1954 | 1955 | // Mark setup as complete 1956 | scope.ppmod_onportal_flag <- true; 1957 | 1958 | } 1959 | 1960 | }); 1961 | 1962 | } 1963 | 1964 | /*******************/ 1965 | // World interface // 1966 | /*******************/ 1967 | 1968 | // Creates an entity using a console command, returns a promise that resolves to its handle 1969 | ::ppmod.create <- function (cmd, key = null) { 1970 | 1971 | // Validate the input arguments 1972 | if (typeof cmd != "string") throw "create: Invalid command argument"; 1973 | if (key != null && typeof key != "string") throw "create: Invalid key argument"; 1974 | 1975 | // The key is the string used to look for the entity after spawning 1976 | // If no key is provided, we guess it from the input command 1977 | if (key == null) { 1978 | // Get the first 17 characters (or less) of the command 1979 | switch (cmd.slice(0, min(cmd.len(), 17))) { 1980 | 1981 | // These commands need to be handled separately 1982 | case "ent_create_portal": key = "cube"; break; 1983 | case "ent_create_paint_": key = "prop_paint_bomb"; break; 1984 | 1985 | default: 1986 | // If the command has an argument, use that as the key 1987 | if (cmd.find(" ") != null) { 1988 | key = cmd.slice(cmd.find(" ") + 1); 1989 | // If the argument is a model, prefix key with "models/" 1990 | if (key.slice(-4) == ".mdl") key = "models/" + key; 1991 | break; 1992 | } 1993 | // If provided only a model, assume we're using prop_dynamic_create 1994 | if (cmd.slice(-4) == ".mdl") { 1995 | key = "models/" + cmd; 1996 | cmd = "prop_dynamic_create " + cmd; 1997 | break; 1998 | } 1999 | // If all else fails, assume we're provided a classname, use ent_create 2000 | key = cmd; 2001 | cmd = "ent_create " + cmd; 2002 | break; 2003 | 2004 | } 2005 | } 2006 | 2007 | // Send the console command to create the entity 2008 | SendToConsole(cmd); 2009 | 2010 | /** 2011 | * Find the entity by passing the key to ppmod.prev. We send this as a 2012 | * console command to take advantage of how console commands are executed 2013 | * synchronously. This lets us make sure that the entity has spawned and 2014 | * that we start looking for it as soon as we can. 2015 | */ 2016 | return ppromise(function (resolve, reject):(cmd, key) { 2017 | SendToConsole("script ppmod.scrq_get("+ ppmod.scrq_add(resolve, 1) +")(ppmod.prev(\""+ key +"\"))"); 2018 | }); 2019 | 2020 | } 2021 | 2022 | // Creates entities in bulk using game_player_equip 2023 | // Returns a ppromise which resolves to a table of arrays with the created entities 2024 | ::ppmod.give <- function (ents) { 2025 | 2026 | // Validate input table 2027 | if (typeof ents != "table") throw "give: Invalid entity table"; 2028 | 2029 | // This procedure requires a player handle, get the first available one 2030 | local player = Entities.FindByClassname(null, "player"); 2031 | // Validate the player instance found to prevent game crashes 2032 | if (!ppmod.validate(player)) throw "give: Failed to find valid player instance"; 2033 | // Create a temporary game_player_equip instance 2034 | local equip = Entities.CreateByClassname("game_player_equip"); 2035 | 2036 | // Assign keyvalues from the input table 2037 | // game_player_equip uses keyvalue pairs to determine spawn quantities 2038 | foreach (classname,idx in ents) { 2039 | equip.__KeyValueFromInt(classname, ents[classname]); 2040 | } 2041 | 2042 | // Spawn the items, then kill the entity 2043 | EntFireByHandle(equip, "Use", "", 0.0, player, null); 2044 | EntFireByHandle(equip, "Kill", "", 0.0, null, null); 2045 | 2046 | return ppromise(function (resolve, reject):(ents) { 2047 | // Use runscript to ensure we're retrieving the entities after creating them 2048 | ppmod.runscript("worldspawn", function ():(resolve, ents) { 2049 | 2050 | // Create an output table 2051 | local output = {}; 2052 | // Entity iterator 2053 | local ent = null; 2054 | 2055 | // Iterate over each spawned class to fetch the entities into an array 2056 | foreach (classname,idx in ents) { 2057 | // Allocate an array for the entities 2058 | output[classname] <- array(ents[classname]); 2059 | // Iterate through all entities with a matching classname 2060 | local i = 0; 2061 | while (ent = Entities.FindByClassname(ent, classname)) { 2062 | output[classname][i] = ent; 2063 | /** 2064 | * Overflow the pointer once we've reached the desired spawn amount. 2065 | * This effectively makes it so that only the last entities of this 2066 | * search remain in the array, albeit in no specific order. 2067 | */ 2068 | if (++i == ents[classname]) i = 0; 2069 | } 2070 | } 2071 | // Resolve the ppromise with the output table 2072 | resolve(output); 2073 | 2074 | }); 2075 | }); 2076 | 2077 | } 2078 | 2079 | // Creates a brush entity 2080 | ::ppmod.brush <- function (pos, size, type = "func_brush", ang = Vector(), create = false) { 2081 | 2082 | // Validate input arguments 2083 | if (typeof pos != "Vector") throw "brush: Invalid position argument"; 2084 | if (typeof size != "Vector") throw "brush: Invalid size argument"; 2085 | if (size.x < 0.0 || size.y < 0.0 || size.z < 0.0) throw "brush: Size must be positive on all axis"; 2086 | // The type argument may be either an entity handle or a string 2087 | if (!ppmod.validate(type) && typeof type != "string") throw "brush: Invalid brush type argument"; 2088 | 2089 | // If the create flag is set, use ppmod.create instead of CreateByClassname, 2090 | // then call this same function again with the new brush and resolve with that. 2091 | if (create) return ppromise(function (resolve, reject):(type, pos, size, ang) { 2092 | ppmod.create(type).then(function (ent):(pos, size, ang, resolve) { 2093 | resolve(ppmod.brush(pos, size, ent, ang)); 2094 | }); 2095 | }); 2096 | 2097 | // If brush type was provided as a string, create a new brush 2098 | // Otherwise, this will continue using `type` as a brush entity 2099 | if (typeof type == "string") { 2100 | type = Entities.CreateByClassname(type); 2101 | } 2102 | 2103 | // Make the brush solid and rotatable 2104 | type.__KeyValueFromInt("Solid", 3); 2105 | // Set the position and angles of the brush 2106 | type.SetAbsOrigin(pos); 2107 | type.SetAngles(ang.x, ang.y, ang.z); 2108 | // Scale the bounding box of the brush, centered on its origin 2109 | type.SetSize(Vector() - size, size); 2110 | 2111 | // Return the entity handle of the new brush 2112 | return type; 2113 | 2114 | } 2115 | 2116 | // Creates a brush entity with trigger properties 2117 | ::ppmod.trigger <- function (pos, size, type = "trigger_once", ang = Vector(), create = false) { 2118 | 2119 | // If the create flag is set, call ppmod.brush with the create flag set 2120 | // and await a response, then call this function again. 2121 | if (create) return ppromise(function (resolve, reject):(pos, size, type, ang) { 2122 | ppmod.brush(pos, size, type, ang, true).then(function (ent):(pos, size, ang, resolve) { 2123 | resolve(ppmod.trigger(pos, size, ent, ang)); 2124 | }); 2125 | }); 2126 | 2127 | // If trigger type was provided as a string, create a new brush 2128 | // Otherwise, this will continue using `type` as a brush entity 2129 | if (typeof type == "string") { 2130 | type = ppmod.brush(pos, size, type, ang); 2131 | } 2132 | 2133 | // Make the trigger non-solid 2134 | type.__KeyValueFromInt("CollisionGroup", 21); 2135 | // Turn on activation by clients by default 2136 | type.__KeyValueFromInt("SpawnFlags", 1); 2137 | // Enable the trigger 2138 | EntFireByHandle(type, "Enable", "", 0.0, null, null); 2139 | 2140 | // If this is a trigger_once, make it disappear upon activation 2141 | if (type.GetClassname() == "trigger_once") { 2142 | type.__KeyValueFromString("OnStartTouch", "!self\x001BKill\x1B\x001B0\x001B1"); 2143 | } 2144 | 2145 | // Return the entity handle of the new trigger 2146 | return type; 2147 | 2148 | } 2149 | 2150 | // Creates and sets up an env_projectedtexture 2151 | ::ppmod.project <- function (material, pos, ang = Vector(90, 0, 0), simple = 0, far = 128.0) { 2152 | 2153 | // Validate input arguments 2154 | if (typeof material != "string") throw "project: Invalid material argument"; 2155 | if (typeof pos != "Vector") throw "project: Invalid position argument"; 2156 | if (typeof ang != "Vector") throw "project: Invalid angles argument"; 2157 | if (typeof simple != "integer" && typeof simple != "boolean") throw "project: Invalid projection type"; 2158 | if (typeof far != "integer" && typeof far != "float") throw "project: Invalid projection distance"; 2159 | 2160 | // Create the env_projectedtexture entity 2161 | local texture = Entities.CreateByClassname("env_projectedtexture"); 2162 | 2163 | // Set the texture position and projection angles 2164 | texture.SetAbsOrigin(pos); 2165 | texture.SetAngles(ang.x, ang.y, ang.z); 2166 | // Set projection distance, projection type, and material name 2167 | texture.__KeyValueFromFloat("FarZ", far); 2168 | texture.__KeyValueFromInt("SimpleProjection", simple.tointeger()); 2169 | texture.__KeyValueFromString("TextureName", material); 2170 | 2171 | // Return a handle to the env_projectedtexture entity 2172 | return texture; 2173 | 2174 | } 2175 | 2176 | // Creates and applies a static decal on a nearby surface 2177 | ::ppmod.decal <- function (material, pos, ang = Vector(90, 0, 0), far = 8.0) { 2178 | 2179 | // Validate input arguments 2180 | if (typeof material != "string") throw "decal: Invalid material argument"; 2181 | if (typeof pos != "Vector") throw "decal: Invalid position argument"; 2182 | if (typeof ang != "Vector") throw "decal: Invalid angles argument"; 2183 | if (typeof far != "integer" && typeof far != "float") throw "decal: Invalid projection distance"; 2184 | 2185 | // Create the info_projecteddecal entity, used for applying the decal 2186 | local decal = Entities.CreateByClassname("info_projecteddecal"); 2187 | 2188 | // Set the decal position and projection angles 2189 | decal.SetAbsOrigin(pos); 2190 | decal.SetAngles(ang.x, ang.y, ang.z); 2191 | // Set the name of the texture to be applied, and the projection distance 2192 | decal.__KeyValueFromString("Texture", material); 2193 | decal.__KeyValueFromFloat("Distance", far); 2194 | // Activate the entity, applying the decal and removing itself 2195 | EntFireByHandle(decal, "Activate", "", 0.0, null, null); 2196 | 2197 | } 2198 | 2199 | // Set up some dummy entites for simplifying ray-through-portal calculations 2200 | // This needs to happen exactly once, else it breaks, thus we use ppmod.onauto 2201 | ppmod.onauto(function () { 2202 | local p_anchor = Entities.CreateByClassname("info_teleport_destination"); 2203 | local r_anchor = Entities.CreateByClassname("info_teleport_destination"); 2204 | 2205 | p_anchor.__KeyValueFromString("Targetname", "ppmod_portals_p_anchor"); 2206 | r_anchor.__KeyValueFromString("Targetname", "ppmod_portals_r_anchor"); 2207 | 2208 | EntFireByHandle(r_anchor, "SetParent", "ppmod_portals_p_anchor", 0.0, null, null); 2209 | }); 2210 | 2211 | // Casts a ray with options for collision with entities, the world, and portals 2212 | ::ppmod.ray <- class { 2213 | 2214 | // Output attributes 2215 | fraction = null; 2216 | point = null; 2217 | entity = null; 2218 | 2219 | // Fractions along the ray for intersections with entities/world 2220 | efrac = 1.0; 2221 | wfrac = 1.0; 2222 | 2223 | // Used for describing the ray 2224 | start = null; 2225 | end = null; 2226 | dir = null; 2227 | len = null; 2228 | div = null; 2229 | 2230 | // Vector components, for simpler iteration through vectors 2231 | static vc = ["x", "y", "z"]; 2232 | // Used for converting angles in degrees to radians 2233 | static deg2rad = PI / 180.0; 2234 | // Used for converting angles in radians to degrees 2235 | static rad2deg = 180.0 / PI; 2236 | // Used to combat floating point calculation errors 2237 | static epsilon = 0.000001; 2238 | 2239 | // Casts a ray which collides with the given axis-aligned bounding box 2240 | // Returns a fraction along the ray where an intersection occurred 2241 | function cast_aabb (bmin, bmax) { 2242 | 2243 | // If the starting point is inside the box, don't proceed 2244 | if ( 2245 | start.x > bmin[0] && start.x < bmax[0] && 2246 | start.y > bmin[1] && start.y < bmax[1] && 2247 | start.z > bmin[2] && start.z < bmax[2] 2248 | ) return 0.0; 2249 | 2250 | // Calculate the distance between the start point and the hit point 2251 | local tmin = [0.0, 0.0, 0.0]; 2252 | local tmax = [0.0, 0.0, 0.0]; 2253 | 2254 | for (local i = 0; i < 3; i ++) { 2255 | if (div[i] >= 0) { 2256 | tmin[i] = (bmin[i] - start[vc[i]]) * div[i]; 2257 | tmax[i] = (bmax[i] - start[vc[i]]) * div[i]; 2258 | } else { 2259 | tmin[i] = (bmax[i] - start[vc[i]]) * div[i]; 2260 | tmax[i] = (bmin[i] - start[vc[i]]) * div[i]; 2261 | } 2262 | if (tmin[0] > tmax[i] || tmin[i] > tmax[0]) return 1.0; 2263 | if (tmin[i] > tmin[0]) tmin[0] = tmin[i]; 2264 | if (tmax[i] < tmax[0]) tmax[0] = tmax[i]; 2265 | } 2266 | 2267 | if (tmin[0] < 0) return 1.0; 2268 | return tmin[0] / len; 2269 | 2270 | } 2271 | 2272 | // Converts the input bbox to an AABB and casts a ray that collides with it 2273 | // Returns a fraction along the ray where an intersection occurred 2274 | function cast_bbox (pos, ang, mins, maxs) { 2275 | 2276 | // Get the farthest coordinate of each bound, effectively obtaining a cube 2277 | local minmin = min(mins.x, min(mins.y, mins.z)); 2278 | local maxmax = max(maxs.x, max(maxs.y, maxs.z)); 2279 | 2280 | // Cheap preemptive check to avoid casting rays if we're far away 2281 | if (pos.x + minmin > max(start.x, end.x)) return 1.0; 2282 | if (pos.x + maxmax < min(start.x, end.x)) return 1.0; 2283 | 2284 | if (pos.y + minmin > max(start.y, end.y)) return 1.0; 2285 | if (pos.y + maxmax < min(start.y, end.y)) return 1.0; 2286 | 2287 | if (pos.z + minmin > max(start.z, end.z)) return 1.0; 2288 | if (pos.z + maxmax < min(start.z, end.z)) return 1.0; 2289 | 2290 | // Precalculate sin/cos functions for the transformation matrix 2291 | local c1 = cos(ang.z); 2292 | local s1 = sin(ang.z); 2293 | local c2 = cos(ang.x); 2294 | local s2 = sin(ang.x); 2295 | local c3 = cos(ang.y); 2296 | local s3 = sin(ang.y); 2297 | 2298 | // Calculate the transformation matrix for resizing the axis-aligned 2299 | // bounding box to cover a rotated object. 2300 | local matrix = [ 2301 | [c2 * c3, c3 * s1 * s2 - c1 * s3, s1 * s3 + c1 * c3 * s2], 2302 | [c2 * s3, c1 * c3 + s1 * s2 * s3, c1 * s2 * s3 - c3 * s1], 2303 | [-s2, c2 * s1, c1 * c2] 2304 | ]; 2305 | 2306 | // These will hold the scaled bounding box with absolute coordinats 2307 | local bmin = [pos.x, pos.y, pos.z]; 2308 | local bmax = [pos.x, pos.y, pos.z]; 2309 | local a, b; 2310 | 2311 | // Perform the matrix transformation 2312 | for (local i = 0; i < 3; i ++) { 2313 | for (local j = 0; j < 3; j ++) { 2314 | a = matrix[i][j] * mins[vc[j]]; 2315 | b = matrix[i][j] * maxs[vc[j]]; 2316 | if (a < b) { 2317 | bmin[i] += a; 2318 | bmax[i] += b; 2319 | } else { 2320 | bmin[i] += b; 2321 | bmax[i] += a; 2322 | } 2323 | } 2324 | } 2325 | 2326 | // Perform the actual raycast 2327 | return cast_aabb(bmin, bmax); 2328 | 2329 | } 2330 | 2331 | // Casts a ray which collides with the AABB of the given entity 2332 | // Returns a fraction along the ray where an intersection occurred 2333 | function cast_ent (ent) { 2334 | 2335 | // Obtain the required parameters and forward the call to cast_bbox 2336 | local frac = cast_bbox( 2337 | ent.GetOrigin(), 2338 | ent.GetAngles() * deg2rad, 2339 | ent.GetBoundingMins(), 2340 | ent.GetBoundingMaxs() 2341 | ); 2342 | 2343 | // If this is the closest hit yet, update the hit entity fraction and handle 2344 | if (frac < efrac) { 2345 | efrac = frac; 2346 | entity = ent; 2347 | } 2348 | 2349 | } 2350 | 2351 | // Handles cases where the entity input field is an array 2352 | function cast_array (arr) { 2353 | 2354 | // Iterate through and handle all array elements 2355 | for (local i = 0; i < arr.len(); i ++) { 2356 | 2357 | // If the start point is inside of a box, no need to continue 2358 | if (efrac == 0.0) break; 2359 | 2360 | // If a valid entity handle was provided, use cast_ent 2361 | if (ppmod.validate(arr[i])) { 2362 | cast_ent(arr[i]); 2363 | continue; 2364 | } 2365 | // If a vector was provided, treat it as a position/size pair 2366 | if (typeof arr[i] == "Vector") { 2367 | local frac = cast_aabb(arr[i] - arr[i+1], arr[i] + arr[i+1]); 2368 | // If this is the closest hit yet, clear the hit entity handle 2369 | if (frac < efrac) { 2370 | efrac = frac; 2371 | entity = null; 2372 | } 2373 | i ++; 2374 | continue; 2375 | } 2376 | // If all else fails, try passing this to ppmod.forent 2377 | ppmod.forent(arr[i], cast_ent.bindenv(this)); 2378 | 2379 | } 2380 | 2381 | } 2382 | 2383 | constructor (start, end, ent = null, world = true, portals = null, ray = null) { 2384 | 2385 | // Validate input arguments 2386 | if (typeof start != "Vector") throw "ray: Invalid start point"; 2387 | if (typeof end != "Vector") throw "ray: Invalid end point"; 2388 | 2389 | // Assign the start/end vectors to the instance 2390 | this.start = start; 2391 | this.end = end; 2392 | 2393 | // Calculate the len and div parameters if not provided 2394 | // These are used for AABB intersection calculations 2395 | if (!ray) { 2396 | dir = this.end - this.start; 2397 | len = dir.Norm(); 2398 | div = [1.0 / dir.x, 1.0 / dir.y, 1.0 / dir.z]; 2399 | } else { 2400 | len = ray[0]; 2401 | div = ray[1]; 2402 | } 2403 | 2404 | do { 2405 | 2406 | // Calculate intersection with the world, if needed 2407 | if (world) wfrac = TraceLine(this.start, this.end, null); 2408 | 2409 | if (ent) { 2410 | // If a valid entity handle was provided, use cast_ent 2411 | if (ppmod.validate(ent)) cast_ent(ent); 2412 | // If an array was provided, use cast_array 2413 | else if (typeof ent == "array") cast_array(ent); 2414 | // If no valid handle was provided, find handles using ppmod.forent 2415 | else ppmod.forent(ent, cast_ent.bindenv(this)); 2416 | } 2417 | 2418 | // Get the fraction of whichever was closest, the entity or world 2419 | fraction = min(efrac, wfrac); 2420 | // Get the point of intersection as a vector 2421 | point = this.start + (this.end - this.start) * fraction; 2422 | // If no entity was hit, clear the handle 2423 | if (wfrac < efrac || efrac == 1.0) entity = null; 2424 | 2425 | // Handle intersections with portals if needed 2426 | if (portals) { 2427 | 2428 | // Validate the portal array 2429 | if (typeof portals != "array") throw "ray: Invalid portals argument"; 2430 | if (portals.len() % 2 != 0) throw "ray: Portals must be provided in sequential pairs"; 2431 | // Convert the array to a pparray if it isn't already 2432 | if (!("indexof" in portals)) portals = pparray(portals); 2433 | 2434 | // Check if we're intersecting the bounding box of one of the provided portals 2435 | local portal = Entities.FindByClassnameWithin(null, "prop_portal", point, 1.0); 2436 | local index = portals.indexof(portal); 2437 | // If this is not in the input portal list, we're done 2438 | if (index == -1) break; 2439 | // Otherwise, find the other linked portal 2440 | local other = portals[index + (index % 2 == 0 ? 1 : -1)]; 2441 | // Validate both portal handles 2442 | if (!ppmod.validate(portal)) throw "ray: Invalid portal handle provided"; 2443 | if (!ppmod.validate(other)) throw "ray: Invalid portal handle provided"; 2444 | 2445 | // Prefetch some vectors 2446 | local otherpos = other.GetOrigin(); 2447 | local othervec = other.GetForwardVector(); 2448 | 2449 | // Obtain anchor entities 2450 | local p_anchor = Entities.FindByName(null, "ppmod_portals_p_anchor"); 2451 | local r_anchor = Entities.FindByName(null, "ppmod_portals_r_anchor"); 2452 | 2453 | // Set portal anchor facing the entry portal 2454 | p_anchor.SetForwardVector(Vector() - portal.GetForwardVector()); 2455 | 2456 | // Set positions of anchors to entry portal origin and ray endpoint, respectively 2457 | p_anchor.SetAbsOrigin(portal.GetOrigin()); 2458 | r_anchor.SetAbsOrigin(point); 2459 | 2460 | // Translate both anchor points to exit portal (r_anchor is parented to p_anchor) 2461 | p_anchor.SetAbsOrigin(otherpos); 2462 | 2463 | // Calculate ray yaw, pitch and roll in degrees 2464 | local yaw = atan2(dir.y, dir.x) * rad2deg; 2465 | local pitch = asin(-dir.z) * rad2deg; 2466 | local roll = atan2(dir.z, dir.Length2D()) * rad2deg; 2467 | 2468 | // Due to being parented, r_anchor's angles are usually relative to p_anchor 2469 | // The "angles" keyvalue, however, is absolute 2470 | r_anchor.__KeyValueFromString("angles", pitch + " " + yaw + " " + roll); 2471 | // Finally, rotate the portal anchor to get ray starting position and direction 2472 | p_anchor.SetForwardVector(othervec); 2473 | 2474 | // The ray anchor now defines the new ray's parameters 2475 | this.start = r_anchor.GetOrigin(); 2476 | dir = r_anchor.GetForwardVector(); 2477 | 2478 | // Check if the new starting point is behind the exit portal 2479 | // If so, a portal intersection has not occurred, we're done 2480 | local offset = this.start - otherpos; 2481 | 2482 | if (othervec.x > epsilon && offset.x < -epsilon) break; 2483 | if (othervec.x < -epsilon && offset.x > epsilon) break; 2484 | 2485 | if (othervec.y > epsilon && offset.y < -epsilon) break; 2486 | if (othervec.y < -epsilon && offset.y > epsilon) break; 2487 | 2488 | if (othervec.z > epsilon && offset.z < -epsilon) break; 2489 | if (othervec.z < -epsilon && offset.z > epsilon) break; 2490 | 2491 | // Apply the new ray parameters and cast the rays again 2492 | len *= 1.0 - fraction; 2493 | this.end = this.start + dir * len; 2494 | div = [1.0 / dir.x, 1.0 / dir.y, 1.0 / dir.z]; 2495 | 2496 | } 2497 | 2498 | // Repeat the loop as long as portal intersections remain relevant 2499 | } while (portals != null); 2500 | 2501 | } 2502 | 2503 | } 2504 | 2505 | // Returns true if the OBBs of two entities intersect, false otherwise 2506 | ::ppmod.intersect <- function (ent1, ent2) { 2507 | 2508 | // Validate input arguments 2509 | if (!ppmod.validate(ent1)) throw "intersect: Invalid first entity handle"; 2510 | if (!ppmod.validate(ent2)) throw "intersect: Invalid second entity handle"; 2511 | 2512 | // Get local axes for each entity 2513 | // The forward, left, and up vectors represent the X, Y, and Z axes respectively 2514 | local vec1 = [ ent1.GetForwardVector(), ent1.GetLeftVector(), ent1.GetUpVector() ]; 2515 | local vec2 = [ ent2.GetForwardVector(), ent2.GetLeftVector(), ent2.GetUpVector() ]; 2516 | 2517 | // Calculate center and half-widths for first entity 2518 | local mins1 = ent1.GetBoundingMins(); 2519 | local maxs1 = ent1.GetBoundingMaxs(); 2520 | local center1 = ent1.GetOrigin() + (mins1 + maxs1) * 0.5; 2521 | local size1 = [ 2522 | (maxs1.x - mins1.x) * 0.5, 2523 | (maxs1.y - mins1.y) * 0.5, 2524 | (maxs1.z - mins1.z) * 0.5 2525 | ]; 2526 | 2527 | // Calculate center and half-widths for second entity 2528 | local mins2 = ent2.GetBoundingMins(); 2529 | local maxs2 = ent2.GetBoundingMaxs(); 2530 | local center2 = ent2.GetOrigin() + (mins2 + maxs2) * 0.5; 2531 | local size2 = [ 2532 | (maxs2.x - mins2.x) * 0.5, 2533 | (maxs2.y - mins2.y) * 0.5, 2534 | (maxs2.z - mins2.z) * 0.5 2535 | ]; 2536 | 2537 | // Calculate rotation matrix between entity relative rotations 2538 | local R = array(3); 2539 | for (local i = 0; i < 3; i++) { 2540 | R[i] = array(3); 2541 | for (local j = 0; j < 3; j++) { 2542 | R[i][j] = vec1[i].Dot(vec2[j]); 2543 | } 2544 | } 2545 | 2546 | // Calculate translation vector between centers in first entity's coordinate frame 2547 | local t = center2 - center1; 2548 | local tA = [ t.Dot(vec1[0]), t.Dot(vec1[1]), t.Dot(vec1[2]) ]; 2549 | 2550 | local ra, rb, tval; 2551 | 2552 | // Test face normals of first entity 2553 | for (local i = 0; i < 3; i++) { 2554 | ra = size1[i]; 2555 | rb = size2[0] * fabs(R[i][0]) + size2[1] * fabs(R[i][1]) + size2[2] * fabs(R[i][2]); 2556 | if (fabs(tA[i]) > ra + rb) return false; 2557 | } 2558 | // Test face normals of second entity 2559 | for (local j = 0; j < 3; j++) { 2560 | ra = size1[0] * fabs(R[0][j]) + size1[1] * fabs(R[1][j]) + size1[2] * fabs(R[2][j]); 2561 | rb = size2[j]; 2562 | local tB = t.Dot(vec2[j]); 2563 | if (fabs(tB) > ra + rb) return false; 2564 | } 2565 | 2566 | // Test the cross-product axes 2567 | for (local i = 0; i < 3; i++) { 2568 | for (local j = 0; j < 3; j++) { 2569 | // Determine indices for the remaining axes 2570 | local i1 = (i + 1) % 3; 2571 | local i2 = (i + 2) % 3; 2572 | local j1 = (j + 1) % 3; 2573 | local j2 = (j + 2) % 3; 2574 | 2575 | ra = size1[i1] * fabs(R[i2][j]) + size1[i2] * fabs(R[i1][j]); 2576 | rb = size2[j1] * fabs(R[i][j2]) + size2[j2] * fabs(R[i][j1]); 2577 | tval = fabs(tA[i2] * R[i1][j] - tA[i1] * R[i2][j]); 2578 | if (tval > ra + rb) return false; 2579 | } 2580 | } 2581 | 2582 | // If no separating axis is found, the boxes intersect 2583 | return true; 2584 | 2585 | } 2586 | 2587 | // Returns true if the given point is inbounds, false otherwise 2588 | ::ppmod.inbounds <- function (point) { 2589 | 2590 | // Validate input argument 2591 | if (typeof point != "Vector") throw "inbounds: Invalid point argument"; 2592 | 2593 | // Cast long rays in all cardinal directions 2594 | // If the point is out of bounds, at least one of these very likely won't collide 2595 | if (TraceLine(point, point + Vector(65536, 0, 0), null) == 1.0) return false; 2596 | if (TraceLine(point, point - Vector(65536, 0, 0), null) == 1.0) return false; 2597 | if (TraceLine(point, point + Vector(0, 65536, 0), null) == 1.0) return false; 2598 | if (TraceLine(point, point - Vector(0, 65536, 0), null) == 1.0) return false; 2599 | if (TraceLine(point, point + Vector(0, 0, 65536), null) == 1.0) return false; 2600 | if (TraceLine(point, point - Vector(0, 0, 65536), null) == 1.0) return false; 2601 | 2602 | // If all of the rays collided, the point is likely inbounds 2603 | return true; 2604 | 2605 | } 2606 | 2607 | // Returns true if the given point is within line of sight, false otherwise 2608 | ::ppmod.visible <- function (eyes, dest, fov = 90.0) { 2609 | 2610 | // Validate input arguments 2611 | if (!ppmod.validate(eyes)) throw "visible: Invalid entity handle"; 2612 | if (typeof dest != "Vector") throw "visible: Invalid destination point"; 2613 | if (typeof fov != "float" && typeof fov != "integer") throw "visible: Invalid FOV argument"; 2614 | 2615 | // Obtain the starting point and ray forward vector 2616 | local start = eyes.GetOrigin(); 2617 | local fvec = (dest - start).Normalize(); 2618 | 2619 | // Check if the destination is within the field of view 2620 | if (eyes.GetForwardVector().Dot(fvec) < cos(fov * PI / 360.0)) return false; 2621 | 2622 | // Casts a ray which passes through thin walls (glass, grates, etc.) 2623 | local frac, point; 2624 | do { 2625 | 2626 | // Cast a ray that collides with the world 2627 | frac = TraceLine(start, dest, null); 2628 | // If the ray didn't hit anything, we're done 2629 | if (frac == 1.0) break; 2630 | 2631 | // Set the starting point to the intersection point, with a 16 unit offset 2632 | point = start + (dest - start) * frac; 2633 | start = point + fvec * 16.0; 2634 | 2635 | // Cast a ray 8 units backwards, which only succeeds if the wall is thin 2636 | } while (TraceLine(point + fvec * 16.0, point + fvec * 8.0, null) != 0.0); 2637 | 2638 | // True if the ray didn't hit anything 2639 | return frac == 1.0; 2640 | 2641 | } 2642 | 2643 | // Creates a button prop and fixes common issues associated with spawning buttons dynamically 2644 | ::ppmod.button <- function (type, pos, ang = Vector()) { 2645 | 2646 | // Ensure that sounds are precached by creating a dummy entity 2647 | ppmod.create(type).then(function (dummy) { 2648 | dummy.Destroy(); 2649 | }); 2650 | 2651 | // Determine the model for the prop 2652 | local model; 2653 | switch (type) { 2654 | case "prop_button": { model = "props/switch001.mdl"; break; } 2655 | case "prop_under_button": { model = "props_underground/underground_testchamber_button.mdl"; break; } 2656 | case "prop_floor_button": { model = "props/portal_button.mdl"; break; } 2657 | case "prop_floor_cube_button":{ model = "props/box_socket.mdl"; break; } 2658 | case "prop_floor_ball_button":{ model = "props/ball_button.mdl"; break; } 2659 | case "prop_under_floor_button": { model = "props_underground/underground_floor_button.mdl"; break; } 2660 | default: throw "button: Invalid button type"; 2661 | } 2662 | 2663 | // Validate position and angle arguments 2664 | if (typeof pos != "Vector") throw "button: Invalid position argument"; 2665 | if (typeof ang != "Vector") throw "button: Invalid angles argument"; 2666 | 2667 | // Return a promise that resolves to a table of button interface methods 2668 | return ppromise(function (resolve, reject):(type, pos, ang, model) { 2669 | 2670 | // First, create a prop_dynamic with the appropriate model 2671 | ppmod.create(model).then(function (ent):(type, pos, ang, resolve) { 2672 | 2673 | // Position the new prop 2674 | ent.SetAbsOrigin(pos); 2675 | ent.SetAngles(ang.x, ang.y, ang.z); 2676 | 2677 | // The floor buttons often come with additional phys_bone_followers 2678 | // Find and position these by iterating through entities backwards 2679 | while (ent.GetClassname() == "phys_bone_follower") { 2680 | ent = ppmod.prev(ent.GetModelName(), ent); 2681 | ent.SetAbsOrigin(pos); 2682 | ent.SetAngles(ang.x, ang.y, ang.z); 2683 | } 2684 | 2685 | // Handle pedestal buttons 2686 | if (type == "prop_button" || type == "prop_under_button") { 2687 | 2688 | // func_button breaks when created dynamically, use func_rot_button instead 2689 | ppmod.brush(pos + (ent.GetUpVector() * 40), Vector(8, 8, 8), "func_rot_button", ang, true).then(function (button):(type, ent, resolve) { 2690 | 2691 | // Make the button box non-solid and activated with +use 2692 | button.__KeyValueFromInt("CollisionGroup", 2); 2693 | button.__KeyValueFromInt("SpawnFlags", 1024); 2694 | EntFireByHandle(button, "SetParent", "!activator", 0.0, ent, null); 2695 | 2696 | // Button properties are stored in a shared table 2697 | local properties = { 2698 | delay = 1.0, 2699 | timer = false, 2700 | permanent = false 2701 | }; 2702 | 2703 | ppmod.addscript(button, "OnPressed", function ():(type, ent, button, properties) { 2704 | 2705 | // Underground buttons have different animation names 2706 | // The additional sound effects for those are baked into the animation 2707 | if (type == "prop_button") EntFireByHandle(ent, "SetAnimation", "down", 0.0, null, null); 2708 | else EntFireByHandle(ent, "SetAnimation", "press", 0.0, null, null); 2709 | button.EmitSound("Portal.button_down"); 2710 | 2711 | // To disable the button while pressed, clear its "+use activates" flag 2712 | button.__KeyValueFromInt("SpawnFlags", 0); 2713 | 2714 | // Simulate the timer ticks 2715 | local timer = null; 2716 | if (properties.timer) { 2717 | 2718 | // Create a logic_timer for repeated ticks 2719 | timer = Entities.CreateByClassname("logic_timer"); 2720 | ppmod.addscript(timer, "OnTimer", function ():(button) { 2721 | button.EmitSound("Portal.room1_TickTock"); 2722 | }); 2723 | 2724 | // Offset activation by one tick to prevent an extra tick upon release 2725 | EntFireByHandle(timer, "RefireTime", "1", 0.0, null, null); 2726 | EntFireByHandle(timer, "Enable", "", FrameTime(), null, null); 2727 | 2728 | } 2729 | 2730 | // If "permanent", skip the release code 2731 | if (properties.permanent) return; 2732 | 2733 | ppmod.wait(function ():(ent, button, type, timer) { 2734 | 2735 | if (type == "prop_button") EntFireByHandle(ent, "SetAnimation", "up", 0.0, null, null); 2736 | else EntFireByHandle(ent, "SetAnimation", "release", 0.0, null, null); 2737 | button.EmitSound("Portal.button_up"); 2738 | 2739 | button.__KeyValueFromInt("SpawnFlags", 1024); 2740 | if (timer) timer.Destroy(); 2741 | 2742 | }, properties.delay); 2743 | 2744 | }); 2745 | 2746 | // Resolve the promise with a table of interface methods 2747 | resolve({ 2748 | 2749 | GetButton = function ():(button) { return button }, 2750 | GetProp = function ():(ent) { return ent }, 2751 | SetDelay = function (delay):(properties) { properties.delay = delay }, 2752 | SetTimer = function (enabled):(properties) { properties.timer = enabled }, 2753 | SetPermanent = function (enabled):(properties) { properties.permanent = enabled }, 2754 | OnPressed = function (scr):(button) { ppmod.addscript(button, "OnPressed", scr) }, 2755 | 2756 | }); 2757 | 2758 | }); 2759 | 2760 | // Handle floor buttons 2761 | } else { 2762 | 2763 | // This moves the phys_bone_followers into place 2764 | EntFireByHandle(ent, "SetAnimation", "BindPose", 0.0, null, null); 2765 | 2766 | // Adjust trigger size based on button type 2767 | local trigger; 2768 | if (type == "prop_under_floor_button") { 2769 | trigger = ppmod.trigger(pos + Vector(0, 0, 8.5), Vector(30, 30, 8.5), "trigger_multiple", ang); 2770 | } else { 2771 | trigger = ppmod.trigger(pos + Vector(0, 0, 7), Vector(20, 20, 7), "trigger_multiple", ang); 2772 | } 2773 | 2774 | // Activated by players and physics props 2775 | trigger.__KeyValueFromInt("SpawnFlags", 9); 2776 | 2777 | // Button properties are stored in a shared table 2778 | local properties = { count = 0 }; 2779 | 2780 | // Used for attaching output scripts to press and unpress events 2781 | local pressrl = Entities.CreateByClassname("logic_relay"); 2782 | pressrl.__KeyValueFromInt("SpawnFlags", 2); 2783 | local unpressrl = Entities.CreateByClassname("logic_relay"); 2784 | unpressrl.__KeyValueFromInt("SpawnFlags", 2); 2785 | 2786 | // Handles something entering the trigger volume 2787 | local press = function ():(type, ent, properties, pressrl) { 2788 | 2789 | // Increment the counter and check if this is the first press 2790 | if (++properties.count != 1) return; 2791 | 2792 | // Trigger the button press relay 2793 | EntFireByHandle(pressrl, "Trigger", "", 0.0, null, null); 2794 | 2795 | // Play the corresponding animations and sounds 2796 | if (type == "prop_under_floor_button") { 2797 | EntFireByHandle(ent, "SetAnimation", "press", 0.0, null, null); 2798 | ent.EmitSound("Portal.OGButtonDepress"); 2799 | } else { 2800 | EntFireByHandle(ent, "SetAnimation", "down", 0.0, null, null); 2801 | ent.EmitSound("Portal.ButtonDepress"); 2802 | } 2803 | 2804 | }; 2805 | 2806 | // Handles something leaving the trigger volume 2807 | local unpress = function ():(type, ent, properties, unpressrl) { 2808 | 2809 | // Decrement the counter and check if the trigger is empty 2810 | if (--properties.count != 0) return; 2811 | 2812 | // Trigger the button press relay 2813 | EntFireByHandle(unpressrl, "Trigger", "", 0.0, null, null); 2814 | 2815 | // Play the corresponding animations and sounds 2816 | if (type == "prop_under_floor_button") { 2817 | EntFireByHandle(ent, "SetAnimation", "release", 0.0, null, null); 2818 | ent.EmitSound("Portal.OGButtonRelease"); 2819 | } else { 2820 | EntFireByHandle(ent, "SetAnimation", "up", 0.0, null, null); 2821 | ent.EmitSound("Portal.ButtonRelease"); 2822 | } 2823 | 2824 | }; 2825 | 2826 | // Checks classnames and model names to filter the entities activating the button 2827 | local strpress, strunpress; 2828 | if (type == "prop_floor_button" || type == "prop_under_floor_button") { 2829 | strpress = "if (self.GetClassname() == \"prop_weighted_cube\" || self.GetClassname() == \"player\") ppmod.scrq_get(" + ppmod.scrq_add(press) + ")()"; 2830 | strunpress = "if (self.GetClassname() == \"prop_weighted_cube\" || self.GetClassname() == \"player\") ppmod.scrq_get(" + ppmod.scrq_add(unpress) + ")()"; 2831 | } else if (type == "prop_floor_ball_button") { 2832 | strpress = "if (self.GetClassname() == \"prop_weighted_cube\" && self.GetModelName() == \"models/props_gameplay/mp_ball.mdl\") ppmod.scrq_get(" + ppmod.scrq_add(press) + ")()"; 2833 | strunpress = "if (self.GetClassname() == \"prop_weighted_cube\" && self.GetModelName() == \"models/props_gameplay/mp_ball.mdl\") ppmod.scrq_get(" + ppmod.scrq_add(unpress) + ")()"; 2834 | } else { 2835 | strpress = "if (self.GetClassname() == \"prop_weighted_cube\" && self.GetModelName() != \"models/props_gameplay/mp_ball.mdl\") ppmod.scrq_get(" + ppmod.scrq_add(press) + ")()"; 2836 | strunpress = "if (self.GetClassname() == \"prop_weighted_cube\" && self.GetModelName() != \"models/props_gameplay/mp_ball.mdl\") ppmod.scrq_get(" + ppmod.scrq_add(unpress) + ")()"; 2837 | } 2838 | ppmod.addoutput(trigger, "OnStartTouch", "!activator", "RunScriptCode", strpress); 2839 | ppmod.addoutput(trigger, "OnEndTouch", "!activator", "RunScriptCode", strunpress); 2840 | 2841 | // Resolve the promise with a table of interface methods 2842 | resolve({ 2843 | 2844 | GetTrigger = function ():(trigger) { return trigger }, 2845 | GetProp = function ():(ent) { return ent }, 2846 | GetCount = function ():(properties) { return properties.count }, 2847 | OnPressed = function (scr):(pressrl) { ppmod.addscript(pressrl, "OnTrigger", scr) }, 2848 | OnUnpressed = function (scr):(unpressrl) { ppmod.addscript(unpressrl, "OnTrigger", scr) }, 2849 | 2850 | }); 2851 | 2852 | } 2853 | 2854 | }); 2855 | 2856 | }); 2857 | 2858 | } 2859 | 2860 | // Launches a physics prop in the given direction. 2861 | ::ppmod.catapult <- function (ent, vec) { 2862 | 2863 | // Validate arguments 2864 | if (typeof vec != "Vector") throw "catapult: Invalid vector argument"; 2865 | 2866 | // Use ppmod.forent to find entity handles if necessary 2867 | if (!ppmod.validate(ent)) { 2868 | ppmod.forent(ent, function (curr):(vec) { 2869 | ppmod.catapult(curr, vec); 2870 | }); 2871 | return; 2872 | } 2873 | 2874 | // Normalize the vector to get its length, used as the launch speed 2875 | local speed = vec.Norm(); 2876 | 2877 | // Create a small trigger_catapult to perform the launch 2878 | local trigger = Entities.CreateByClassname("trigger_catapult"); 2879 | trigger.__KeyValueFromInt("Solid", 3); 2880 | trigger.SetAbsOrigin(ent.GetOrigin()); 2881 | trigger.SetForwardVector(vec); 2882 | trigger.SetSize(Vector(-0.2, -0.2, -0.2), Vector(0.2, 0.2, 0.2)); 2883 | trigger.__KeyValueFromInt("CollisionGroup", 1); 2884 | 2885 | // Use the trigger's angles for the launch direction 2886 | local ang = trigger.GetAngles(); 2887 | trigger.__KeyValueFromInt("SpawnFlags", 8); 2888 | trigger.__KeyValueFromFloat("PhysicsSpeed", speed); 2889 | trigger.__KeyValueFromString("LaunchDirection", ang.x+" "+ang.y+" "+ang.z); 2890 | 2891 | // Enable the trigger and kill it right away 2892 | EntFireByHandle(trigger, "Enable", "", 0.0, null, null); 2893 | EntFireByHandle(trigger, "Kill", "", 0.0, null, null); 2894 | 2895 | } 2896 | 2897 | // Apply a directional force to a prop, scaled to units per second 2898 | ::ppmod.push <- function (ent, vec) { 2899 | 2900 | // Validate arguments 2901 | if (typeof vec != "Vector") throw "push: Invalid vector argument"; 2902 | 2903 | // Use ppmod.forent to find entity handles if necessary 2904 | if (!ppmod.validate(ent)) { 2905 | return ppmod.forent(ent, function (curr):(vec) { 2906 | ppmod.push(curr, vec); 2907 | }); 2908 | } 2909 | 2910 | // Increase the point_push think time to improve consistency 2911 | // This prevents the pusher from pushing twice when we don't want it to 2912 | SendToConsole("portal_pointpush_think_rate 0.15"); 2913 | 2914 | // Ensure that the prop is awake and mobile 2915 | EntFireByHandle(ent, "Wake", "", 0.0, null, null); 2916 | EntFireByHandle(ent, "EnableMotion", "", 0.0, null, null); 2917 | 2918 | // Create the point_push entity 2919 | local pusher = Entities.CreateByClassname("point_push"); 2920 | 2921 | // Normalize the vector and get its length to use as the velocity 2922 | local speed = vec.Norm(); 2923 | // Position the pusher at the origin of the prop 2924 | pusher.SetAbsOrigin(ent.GetOrigin()); 2925 | pusher.SetForwardVector(vec); 2926 | pusher.__KeyValueFromFloat("Radius", 0.1); 2927 | // Scale the velocity by a constant factor to use as pusher magnitude 2928 | pusher.__KeyValueFromFloat("Magnitude", speed * 0.3005); 2929 | pusher.__KeyValueFromInt("SpawnFlags", 22); 2930 | // Parent the pusher to the entity in case it moves during setup 2931 | EntFireByHandle(pusher, "SetParent", "!activator", 0.0, ent, null); 2932 | // Enable the pusher and kill it after one push has occurred 2933 | EntFireByHandle(pusher, "Enable", "", 0.0, null, null); 2934 | EntFireByHandle(pusher, "Kill", "", 0.1, null, null); 2935 | 2936 | } 2937 | 2938 | /******************/ 2939 | // Game interface // 2940 | /******************/ 2941 | 2942 | // Displays text on a player's screen using the game_text entity 2943 | ::ppmod.text <- class { 2944 | 2945 | ent = null; 2946 | 2947 | constructor (text = "", x = -1.0, y = -1.0) { 2948 | // Create the game_text entity and configure defaults 2949 | this.ent = Entities.CreateByClassname("game_text"); 2950 | this.ent.__KeyValueFromString("Message", text); 2951 | this.ent.__KeyValueFromString("Color", "255 255 255"); 2952 | this.ent.__KeyValueFromFloat("X", x); 2953 | this.ent.__KeyValueFromFloat("Y", y); 2954 | } 2955 | 2956 | // Returns the internal game_text entity 2957 | function GetEntity () { 2958 | return this.ent; 2959 | } 2960 | // Sets the position of the text along the X/Y axis 2961 | function SetPosition (x, y) { 2962 | this.ent.__KeyValueFromFloat("X", x); 2963 | this.ent.__KeyValueFromFloat("Y", y); 2964 | } 2965 | // Changes the displayed string 2966 | function SetText (text) { 2967 | this.ent.__KeyValueFromString("Message", text); 2968 | } 2969 | // Sets a font size (0 to 5) by adjusting the channel 2970 | function SetSize (size) { 2971 | // Channels sorted from smallest to biggest font size 2972 | this.ent.__KeyValueFromInt("Channel", [2, 1, 4, 0, 5, 3][size]); 2973 | } 2974 | // Sets primary text color and, optionally, secondary color 2975 | function SetColor (c1, c2 = null) { 2976 | this.ent.__KeyValueFromString("Color", c1); 2977 | if (c2) this.ent.__KeyValueFromString("Color2", c2); 2978 | } 2979 | // Sets the fade in/out effect, optionally switches between effect type 2980 | function SetFade (fin, fout, fx = false) { 2981 | this.ent.__KeyValueFromFloat("FadeIn", fin); 2982 | this.ent.__KeyValueFromFloat("FXTime", fin); 2983 | this.ent.__KeyValueFromFloat("FadeOut", fout); 2984 | if (fx) this.ent.__KeyValueFromInt("Effect", 2); 2985 | else this.ent.__KeyValueFromInt("Effect", 0); 2986 | } 2987 | // Displays the text for the given time to the given observer 2988 | function Display (hold = null, player = null) { 2989 | if (hold == null) hold = FrameTime(); 2990 | this.ent.__KeyValueFromFloat("HoldTime", hold); 2991 | if (player) this.ent.__KeyValueFromInt("SpawnFlags", 0); 2992 | else this.ent.__KeyValueFromInt("SpawnFlags", 1); 2993 | EntFireByHandle(ent, "Display", "", 0.0, player, null); 2994 | } 2995 | 2996 | } 2997 | 2998 | // Creates a console command alias for calling a script function 2999 | ::ppmod.alias <- function (cmd, scr) { 3000 | 3001 | // Validate input argument 3002 | if (typeof cmd != "string") throw "alias: Invalid command argument"; 3003 | 3004 | // Add the input script to the script queue 3005 | // This additionally validates the argument 3006 | local scrq_idx = ppmod.scrq_add(scr, -1); 3007 | // Set up a console alias to call the input script 3008 | SendToConsole("alias \""+ cmd +"\" \"script ppmod.scrq_get("+ scrq_idx +")()\""); 3009 | 3010 | } 3011 | -------------------------------------------------------------------------------- /scripts/vscripts/sl_httportal.nut: -------------------------------------------------------------------------------- 1 | /** 2 | * Reloads the current level if this file is missing after a save load. 3 | * 4 | * Players often believe that mods "didn't uninstall" when they have in 5 | * fact just loaded a save from the menu, which persists script scope. 6 | * This tool aims to solve that by detecting a file structure change. 7 | * 8 | * @author p2r3 9 | */ 10 | 11 | // Exit early if this file has called itself 12 | if (getstackinfos(4) && getstackinfos(4).src == getstackinfos(1).src) return; 13 | 14 | // Ensure we're running on the server's script scope 15 | if (!("Entities" in this)) return; 16 | 17 | // The entrypoint function - called once entity I/O has initialized 18 | ::__slInit <- function () { 19 | /** 20 | * Look for an unnamed "logic_auto" entity for connecting functions to 21 | * run on load. If such an entity is not found, one is created. In this 22 | * case, entity indexes may be offset. If that is a concern, savelock 23 | * should be loaded after code that reads entindex. 24 | */ 25 | local auto = null; 26 | while (auto = Entities.FindByClassname(null, "logic_auto")) { 27 | if (!auto.IsValid()) continue; 28 | if (auto.GetName() == "") break; 29 | } 30 | if (!auto) auto = Entities.CreateByClassname("logic_auto"); 31 | auto.ConnectOutput("OnLoadGame", "__slLoad"); 32 | }; 33 | 34 | // Called after the map has finished loading, on every load 35 | ::__slLoad <- function () { 36 | try { 37 | // Try to include this very same script file 38 | IncludeScript(getstackinfos(1).src); 39 | } catch (e) { 40 | // If it no longer exists, restart the level 41 | SendToConsole("restart_level"); 42 | } 43 | }; 44 | 45 | // Run the entrypoint function as soon as entity I/O starts 46 | EntFireByHandle(Entities.First(), "RunScriptCode", "::__slInit()", 0.0, null, null); 47 | --------------------------------------------------------------------------------