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