├── .gitignore ├── README.md ├── doc ├── .gitignore ├── Makefile ├── _static │ ├── 90-degrees-strafe-radius.png │ ├── handgrenade-vel.png │ ├── hud-1.jpg │ ├── jumpbug.png │ ├── optang-1.png │ ├── qconread.png │ ├── radius-estimate-xy.png │ ├── selfgauss-1.png │ └── veccom-1.png ├── algorithms.rst ├── basicphy.rst ├── collisions.rst ├── conf.py ├── explosions.rst ├── fundamentals.rst ├── glossary.rst ├── health.rst ├── index.rst ├── ladderphy.rst ├── strafing.rst └── tastools.rst ├── injectlib ├── .gitignore ├── Makefile ├── common.hpp ├── customhud.cpp ├── customhud.hpp ├── injectmain.cpp ├── movement.cpp ├── movement.hpp ├── strafemath.cpp ├── strafemath.hpp ├── symutils.cpp └── symutils.hpp └── utils ├── qconread ├── .gitignore ├── logtablemodel.cpp ├── logtablemodel.h ├── logtableview.cpp ├── logtableview.h ├── main.cpp ├── qconread.pro ├── qcreadwin.cpp └── qcreadwin.h └── taslaunch ├── gamecfg.py ├── genlegit.py ├── gensim.py ├── splitscript.py └── taslaunch.py /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.DS_Store -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TasTools mod 2 | 3 | This mod is used as part of the process of Half-Life TAS creation. We do not 4 | consider the demos recorded in TasTools mod to be legitimate. The `genlegit.py` 5 | script is required to produce legitimate scripts. This repo also contains 6 | qconread, which is a Qt5 GUI utility that displays the console output in an 7 | accessible way. 8 | 9 | Assuming Qt5 is installed, you can build qconread in Linux by typing `qmake` in 10 | `utils/qconread` to generate the Makefile. Once the Makefile is generated you 11 | only need `make` for subsequent builds. 12 | 13 | Currently, only Linux is supported. To build the mod, enter the `injectlib` 14 | folder and type `make`. A shared library named `tasinjectlib.so` will be 15 | created. To inject this library into Half-Life, set `LD_PRELOAD` to the path 16 | of this library before running the game. 17 | -------------------------------------------------------------------------------- /doc/.gitignore: -------------------------------------------------------------------------------- 1 | _build 2 | -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | 49 | clean: 50 | rm -rf $(BUILDDIR)/* 51 | 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | dirhtml: 58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 61 | 62 | singlehtml: 63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 64 | @echo 65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 66 | 67 | pickle: 68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 69 | @echo 70 | @echo "Build finished; now you can process the pickle files." 71 | 72 | json: 73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 74 | @echo 75 | @echo "Build finished; now you can process the JSON files." 76 | 77 | htmlhelp: 78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 79 | @echo 80 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 81 | ".hhp project file in $(BUILDDIR)/htmlhelp." 82 | 83 | qthelp: 84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 85 | @echo 86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/TasTools.qhcp" 89 | @echo "To view the help file:" 90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/TasTools.qhc" 91 | 92 | devhelp: 93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 94 | @echo 95 | @echo "Build finished." 96 | @echo "To view the help file:" 97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/TasTools" 98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/TasTools" 99 | @echo "# devhelp" 100 | 101 | epub: 102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 103 | @echo 104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 105 | 106 | latex: 107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 108 | @echo 109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 111 | "(use \`make latexpdf' here to do that automatically)." 112 | 113 | latexpdf: 114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 115 | @echo "Running LaTeX files through pdflatex..." 116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 118 | 119 | latexpdfja: 120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 121 | @echo "Running LaTeX files through platex and dvipdfmx..." 122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 124 | 125 | text: 126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 127 | @echo 128 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 129 | 130 | man: 131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 132 | @echo 133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 134 | 135 | texinfo: 136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 137 | @echo 138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 139 | @echo "Run \`make' in that directory to run these through makeinfo" \ 140 | "(use \`make info' here to do that automatically)." 141 | 142 | info: 143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 144 | @echo "Running Texinfo files through makeinfo..." 145 | make -C $(BUILDDIR)/texinfo info 146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 147 | 148 | gettext: 149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 150 | @echo 151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 152 | 153 | changes: 154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 155 | @echo 156 | @echo "The overview file is in $(BUILDDIR)/changes." 157 | 158 | linkcheck: 159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 160 | @echo 161 | @echo "Link check complete; look for any errors in the above output " \ 162 | "or in $(BUILDDIR)/linkcheck/output.txt." 163 | 164 | doctest: 165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 166 | @echo "Testing of doctests in the sources finished, look at the " \ 167 | "results in $(BUILDDIR)/doctest/output.txt." 168 | 169 | xml: 170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 171 | @echo 172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 173 | 174 | pseudoxml: 175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 176 | @echo 177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 178 | -------------------------------------------------------------------------------- /doc/_static/90-degrees-strafe-radius.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Matherunner/tastools/6d90cc4ed44f2ffa5daad440ecb8fa9c9c989ea9/doc/_static/90-degrees-strafe-radius.png -------------------------------------------------------------------------------- /doc/_static/handgrenade-vel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Matherunner/tastools/6d90cc4ed44f2ffa5daad440ecb8fa9c9c989ea9/doc/_static/handgrenade-vel.png -------------------------------------------------------------------------------- /doc/_static/hud-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Matherunner/tastools/6d90cc4ed44f2ffa5daad440ecb8fa9c9c989ea9/doc/_static/hud-1.jpg -------------------------------------------------------------------------------- /doc/_static/jumpbug.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Matherunner/tastools/6d90cc4ed44f2ffa5daad440ecb8fa9c9c989ea9/doc/_static/jumpbug.png -------------------------------------------------------------------------------- /doc/_static/optang-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Matherunner/tastools/6d90cc4ed44f2ffa5daad440ecb8fa9c9c989ea9/doc/_static/optang-1.png -------------------------------------------------------------------------------- /doc/_static/qconread.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Matherunner/tastools/6d90cc4ed44f2ffa5daad440ecb8fa9c9c989ea9/doc/_static/qconread.png -------------------------------------------------------------------------------- /doc/_static/radius-estimate-xy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Matherunner/tastools/6d90cc4ed44f2ffa5daad440ecb8fa9c9c989ea9/doc/_static/radius-estimate-xy.png -------------------------------------------------------------------------------- /doc/_static/selfgauss-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Matherunner/tastools/6d90cc4ed44f2ffa5daad440ecb8fa9c9c989ea9/doc/_static/selfgauss-1.png -------------------------------------------------------------------------------- /doc/_static/veccom-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Matherunner/tastools/6d90cc4ed44f2ffa5daad440ecb8fa9c9c989ea9/doc/_static/veccom-1.png -------------------------------------------------------------------------------- /doc/algorithms.rst: -------------------------------------------------------------------------------- 1 | Algorithms 2 | ========== 3 | 4 | 5 | Line strafing 6 | ------------- 7 | 8 | Line strafing refers to the act of strafing along a fixed line towards a 9 | particular direction. It is not possible to strafe so that the path is a 10 | straight line, therefore we have to choose the strafing directions (either left 11 | or right) carefully to approximate a straight path and minimise the deviation 12 | from the path. We will describe the approach used by TasTools mod here. 13 | 14 | Recall that every line in :math:`\mathbb{R}^n` can be represented as a 15 | parametric equation :math:`\mathbf{r} = \mathbf{a} + s \mathbf{b}`, such that 16 | the line is the locus of :math:`\mathbf{r}`, :math:`s` is the parameter, 17 | :math:`\mathbf{a}` is a known point on the line and :math:`\mathbf{b}` is a 18 | vector parallel to the line. In the context of line strafing 19 | :math:`\mathbf{a}` is called the *origin of line strafe* (OLS) and 20 | :math:`\mathbf{b}` is the *direction of line strafe* (DLS). For simplicity we 21 | often normalise :math:`\mathbf{b}`. Let :math:`\mathbf{p}` be the player 22 | position, then the distance of the player from the line is given by 23 | 24 | .. math:: \left\lVert (\mathbf{a} - \mathbf{p}) - \left[ (\mathbf{a} - \mathbf{p}) 25 | \cdot \mathbf{\hat{b}} \right] \mathbf{\hat{b}} \right\rVert 26 | 27 | To line strafe, we would use Equation :eq:`newvelmat` to compute two different 28 | player positions corresponding to left and right strafe. Then the distances of 29 | these positions from the line is compared, the direction that causes the 30 | smallest deviation from the line is chosen to be the final strafing direction 31 | for this particular frame. 32 | 33 | The advantage of this method is that it allows some control over the amplitude 34 | of line strafe. If the initial player position is relatively far away from the 35 | line, then the amplitude of the subsequent path will be approximately equal to 36 | the initial point-to-line distance. 37 | 38 | In practice, the method to compute :math:`\mathbf{\hat{b}}` depends on the 39 | value of :math:`\mathbf{v}`. If :math:`\mathbf{v} = \mathbf{0}`, then we might 40 | set :math:`\mathbf{\hat{b}} = \langle \cos\vartheta, \sin\vartheta \rangle`, 41 | which means line strafing towards the initial viewing direciton. Otherwise, we 42 | would set :math:`\mathbf{\hat{b}} = \mathbf{\hat{v}}`. At times we might want 43 | to override :math:`\mathbf{\hat{b}}` manually. In TasTools, this can be 44 | accomplished by issuing ``tas_yaw`` while ``+linestrafe`` is active. 45 | 46 | 47 | Computing strafing inputs 48 | ------------------------- 49 | 50 | Up to this point we have been analysing strafing in terms of :math:`\theta`, 51 | the angle between velocity and acceleration vectors. To actually strafe in 52 | practice, we need to adjust the yaw angle and provide the input (such as 53 | ``+moveright``) correctly. In this section we describe a simple algorithm to 54 | produce such input. 55 | 56 | We start off by computing the intended :math:`\theta` **in degrees**, which is 57 | the foundation of all. Now we must set :math:`F` and :math:`S`, which can be 58 | achieved by issuing the correct commands (``+forward``, ``+moveleft``, 59 | ``+moveright``, ``+back``). Assume that :math:`\lvert S\rvert = \lvert 60 | F\rvert` and :math:`\lVert\langle F,S\rangle\rVert \ge M` before 61 | ``PM_CheckParamters`` is called. In principle it does not matter which of the 62 | four commands are issued, but to minimise screen jittering we can adopt the 63 | following guideline: 64 | 65 | * If :math:`0 \le \theta < 22.5` then ``+back`` with negative ``cl_backspeed``. 66 | * If :math:`22.5 \le \theta < 67.5` then ``+back`` and ``+move(right|left)`` 67 | with negative ``cl_backspeed``. 68 | * If :math:`67.5 \le \theta` then ``+move(right|left)``. 69 | 70 | where 22.5 is midway between 0 and 45, and 67.5 is midway between 45 and 90. 71 | You must be wondering about the ``+back`` and negative ``cl_backspeed`` as 72 | well. The rationale is this: we want to avoid accidentally pushing movable 73 | entities. If you look at the ``CPushable::Move`` function in 74 | ``dlls/func_break.cpp``, notice that an object can be pushed only if 75 | ``+forward`` or ``+use`` is active. 76 | 77 | To compute the new yaw angle, in the most general way we compute 78 | :math:`\alpha`, :math:`\beta` and :math:`\phi`, **all in degrees**, where 79 | 80 | .. math:: \phi = \arctan\left( \left\lvert \frac{S}{F} \right\rvert \right) 81 | \quad\quad 82 | \alpha = \operatorname{atan2}(v_y, v_x) 83 | \quad\quad 84 | \beta = \alpha \pm (\phi - \theta) 85 | 86 | Notice the :math:`\pm` sign in :math:`\beta`? It should be replaced by a 87 | :math:`+` if right strafing and :math:`-` if left strafing. We must add that 88 | if :math:`\mathbf{v} = \mathbf{0}` then we are better off setting :math:`\theta 89 | = 0` and :math:`\alpha = \vartheta`. In practice, when the speed is zero what 90 | we want is to strafe towards the yaw direction. In this case it does not make 91 | sense to have :math:`\theta` carrying nonzero values, even though it is 92 | mathematically valid. The ``atan2`` function is available in most sensible 93 | programming languages. Do not use the plain arc tangent to compute 94 | :math:`\alpha` or you will land yourself in trouble. In addition, we often do 95 | not need to compute :math:`\phi` in the manner presented above if :math:`\lvert 96 | S\rvert = \lvert F\rvert`, unless vectorial compensation is employed (will be 97 | discussed later). 98 | 99 | Now we can compute the following trial new yaw angles, denoted as 100 | :math:`\tilde{\vartheta}_1` and :math:`\tilde{\vartheta}_2`, such that 101 | 102 | .. math:: \tilde{\vartheta}_1 = \operatorname{anglemod}(\beta) 103 | \quad\quad 104 | \tilde{\vartheta}_2 = \operatorname{anglemod}(\beta + \operatorname{sgn}(\beta) u) 105 | 106 | We see a new function anglemod and a new constant :math:`u = 360/65536`. 107 | Anglemod is a function defined in ``pm_shared/pm_math.c`` to wrap angles to 108 | :math:`[0, 360)` degrees. Except, it accomplishes this by means of a faster 109 | approximation, instead of a more expensive but correct method which may involve 110 | ``fmod``. The spirit is not dissimilar to the famous `fast inverse square 111 | root`__ approximation. The angles produced by anglemod is always a multiple of 112 | :math:`u`. Consequently the strafing may be less accurate, now that the actual 113 | :math:`\theta` will only be an approximation of the intended :math:`\theta`. 114 | 115 | __ https://en.wikipedia.org/wiki/Fast_inverse_square_root 116 | 117 | The anglemod function truncates the input angle, instead of rounding it. The 118 | following optimisation can be done to improve the accuracy slightly. Set 119 | :math:`\theta_1 = \tilde{\vartheta}_1 \mp \phi` and :math:`\theta_2 = 120 | \tilde{\vartheta}_2 \mp \phi` which are the actual :math:`\theta`\ s, then use 121 | these :math:`\theta`\ s and the scalar FME to compute the new speeds. Now 122 | compare the new speeds to determine the :math:`\tilde{\vartheta}` that gives 123 | the higher speed. This will be the final yaw angle. 124 | 125 | 126 | Autoactions 127 | ----------- 128 | 129 | An autoaction refers to a set of input that are generated automatically when 130 | certain conditions are met. In TasTools they are (in order of precedence) 131 | ``tas_jb``, ``tas_lgagst``, ``tas_db4l``, ``tas_db4c``, ``tas_dtap``, 132 | ``tas_dwj`` and ``tas_cjmp``. The tables below show the conditions and 133 | corresponding actions. If a condition is not displayed, no-op is assumed. 134 | Key: ``og`` is onground status, ``d`` is user ``+duck`` input, ``j`` is user 135 | ``+jump`` input, and ``dst`` is duckstate. The abbreviation "a.u." stands for 136 | "after unducking", while "a.d." stands for "after ducking". 137 | 138 | When we say a command is taken precedence over the other, it means if the 139 | former command performs an action all lower precedence commands will be 140 | inhibited. 141 | 142 | Implementing automatic jumpbug can be tricky. Suppose ``+duck`` is not active, 143 | player is not onground and falling. We want to make sure the player is not 144 | onground after the final groundcheck, which requires predicting the new 145 | position after ``PM_AddCorrectGravity`` and movement physics. The following is 146 | the action table for jumpbug implementation. Jumpbug is usually prioritised 147 | over other autoactions. If :math:`v_z > 180` then jumpbug is impossible. 148 | 149 | 1. If dst 2 AND unduckable AND jumpable AND onground with dst 0, stop with 150 | ``-duck`` and ``+jump``. 151 | 2. If new position is unduckable AND new position is onground with dst 0, stop 152 | with ``+duck`` and ``-jump``. 153 | 154 | Automatic ducktap is taken priority over automatic jump unless stated 155 | otherwise. Recall that ducktapping only works if there exists sufficient space 156 | to accomodate the player bounding box if he is moved vertically up by 18 units, 157 | while the duckstate is not 2. If the duckstate is 2 then the player must first 158 | unduck for this frame. Ducktap is relatively complex: 159 | 160 | == == == === ====== 161 | og d j dst action 162 | == == == === ====== 163 | 0 0 1 2 ``-jump`` if unduckable AND onground a.u.. 164 | 1 -- -- 0 ``-jump`` and ``+duck`` if sufficient space, automatic jump otherwise. 165 | 1 -- -- 1 ``-duck`` and decrement if sufficient space., automatic jump otherwise. 166 | 1 -- -- 2 ``-jump`` and ``-duck`` if unduckable AND sufficient space a.u., automatic jump otherwise. 167 | == == == === ====== 168 | 169 | For automatic jumping, the need to handle ``pmove->oldbuttons`` complicates 170 | matters. At the time of writing, TasTools assumes ``IN_JUMP`` is unset in 171 | ``pmove->oldbuttons``. A rare use case for this would be to temporarily 172 | disable automatic jumping simply by issuing ``+jump``. 173 | 174 | == == == === ====== 175 | og d j dst action 176 | == == == === ====== 177 | 0 0 1 0 ``-jump`` if new position is onground with dst 0. 178 | 0 1 1 2 ``-jump`` if new position is onground with dst 2. 179 | 0 0 -- 2 Decrement and ``+jump`` if unduckable AND onground a.u. AND jumpable. ``-jump`` if new position is unduckable AND new position is onground with dst 0. 180 | 0 1 1 0 ``-jump`` if new position a.d. is onground. 181 | 1 0 -- 1 Decrement and ``+jump`` if jumpable AND insufficient space, ``-jump`` otherwise. 182 | 1 1 -- 1 Decrement and ``+jump`` if jumpable, ``-jump`` otherwise. 183 | 1 -- -- 0/2 Decrement and ``+jump`` if jumpable, ``-jump`` otherwise. 184 | == == == === ====== 185 | 186 | Next we have DB4L. As with jumpbug, if :math:`v_z > 180` then this is 187 | impossible. 188 | 189 | == === ====== 190 | og dst action 191 | == === ====== 192 | 1 2 Decrement and set state to 0 if state is 1 193 | 0 0 ``+duck`` and set state to 1 if new position is obstructed by ground. 194 | 0 2 ``+duck`` and set state to 1 if unduckable AND (onground a.u. OR new position a.u. is obstructed by onground). Otherwise, decrement and set state to 0 if state is 1. 195 | == === ====== 196 | 197 | Then we have DB4C. 198 | 199 | == == === ====== 200 | og d dst action 201 | == == === ====== 202 | 0 0 0 Decrement and ``+duck`` if new position is obstructed OR (new position a.d. is obstructed AND new speed is less than new speed a.d.). 203 | == == === ====== 204 | 205 | We also have DWJ, which is inserting ``+duck`` at the instant the player jumps. 206 | This can be useful for longjump and as a jumping style itself. To selfgauss 207 | with headshot immediately after jumping usually requires this jumping style to 208 | work. There is no action table for this -- the counter is decremented and 209 | ``+duck`` is inserted whenever the player successfully jumps. 210 | 211 | 212 | Vectorial compensation 213 | ---------------------- 214 | 215 | Vectorial compensation (VC) is a novel technique developed to push the strafing 216 | accuracy closer to perfection by further compensating the effects of anglemod. 217 | It is called *vectorial* as it manipulates the values for ``cl_forwardspeed`` 218 | and ``cl_sidespeed``, thereby changing the direction of 219 | :math:`\mathbf{\hat{a}}` slightly. This technique is not implemented in 220 | TasTools, however, as its use can significantly reduce the enjoyability of the 221 | resulting TAS due to the screen shaking haphazardly, effectively transforming 222 | the resulting videos into some psychedelic art. Furthermore, the advantages of 223 | VC over the simple anglemod compensation described previously are largely 224 | negligible. It is for these reasons that we decided against implementing VC in 225 | TasTools, though the technique will still be described here for academic 226 | interests. 227 | 228 | The idea is the following: while the yaw angle in degrees is always a multiple 229 | of :math:`u`, we can adjust the values of ``cl_forwardspeed`` and 230 | ``cl_sidespeed`` in combination with ``cl_yawspeed`` so that the polar angle of 231 | :math:`\mathbf{\hat{a}}` can reside between any two multiples of :math:`u`. As 232 | a result, the actual :math:`\theta` can now be better approximated, hence 233 | higher strafing accuracy. 234 | 235 | Have a look at the illustration below. 236 | 237 | .. image:: _static/veccom-1.png 238 | 239 | The :math:`\mathbf{a}` in the figure is the *intended* :math:`\mathbf{a}`. The 240 | actual :math:`\mathbf{a}` being computed by the game will likely be an 241 | approximation of the intended vector. Also, the spaces between the lines 242 | corresponding to the multiples of :math:`u` are exaggerated so that they are 243 | easier to see. The figure above depicts strafing to the right. 244 | 245 | The algorithm would begin with the decision to strafe left or right, then 246 | compute :math:`\theta` **in degrees**, along with 247 | 248 | .. math:: \alpha = \operatorname{atan2}(v_y, v_x) 249 | \quad\quad 250 | \beta = \alpha \mp \theta 251 | \quad\quad 252 | \sigma = 253 | \begin{cases} 254 | \lceil\beta u^{-1}\rceil u - \beta & \text{if right strafing} \\ 255 | \beta - \lfloor\beta u^{-1}\rfloor u & \text{if left strafing} 256 | \end{cases} 257 | 258 | where the :math:`\mp` in :math:`\beta` should be replaced by :math:`-` if right 259 | strafing and vice versa. The quantity :math:`\sigma` has the following 260 | meaning: :math:`\lceil \beta u^{-1} \rceil u - \beta` represents the difference 261 | between :math:`\beta` and the smallest multiple of :math:`u` not lower than 262 | :math:`\beta`, while the other represents the difference between :math:`\beta` 263 | and the largest multiple of :math:`u` not greater than :math:`\beta`. It is 264 | exactly these differences that we are compensating in this algorithm. 265 | 266 | Now we must find :math:`\phi = \arctan(S/F)` with :math:`F \ge 0` and :math:`S 267 | \ge 0` so that :math:`\phi - \lfloor\phi u^{-1}\rfloor u` closely approximates 268 | :math:`\sigma`. A naive, inefficient and hackish way would be: for all 269 | :math:`(F,S)` pairs with :math:`\lVert\langle F,S\rangle\rVert \ge M`, compute 270 | the associated :math:`\phi - \lfloor\phi u^{-1}\rfloor u` and then find the one 271 | which approximates :math:`\sigma` to the desired accuracy. The problem with 272 | this approach, leaving aside its crudeness, is that there are about 4 million 273 | :math:`(F,S)` pairs that satisfy the above constraint with :math:`M = 320`, 274 | which translates to 4 million arc tangent computations *per frame*. This takes 275 | about 0.25s on a modern 2.0 GHz Core i7. 276 | 277 | TODO 278 | 279 | 280 | Delicious recipes 281 | ----------------- 282 | 283 | We will provide some implementations of basic strafing functions in Python. 284 | ``import math`` is required. 285 | 286 | The following function returns speed after one frame of optimal strafing. 287 | 288 | .. code-block:: python 289 | 290 | def fme_spd_opt(spd, L, tauMA): 291 | tmp = L - tauMA 292 | if tmp < 0: 293 | return math.sqrt(spd * spd + L * L) 294 | if tmp < spd: 295 | return math.sqrt(spd * spd + tauMA * (L + tmp)) 296 | return spd + tauMA 297 | 298 | If computing the velocity vector is required, instead of just the speed, then 299 | one might use the following implementation, where ``d`` is the direction: ``1`` 300 | for right and ``-1`` for left. 301 | 302 | .. code-block:: python 303 | 304 | def fme_vel_opt(v, d, L, tauMA): 305 | tmp = L - tauMA 306 | spd = math.hypot(v[0], v[1]) 307 | ax = 0 308 | ay = 0 309 | if tmp < 0: 310 | ax = L * v[1] * d / spd 311 | ay = -L * v[0] * d / spd 312 | elif tmp < spd: 313 | ct = tmp / spd 314 | st = d * math.sqrt(1 - ct * ct) 315 | ax = tauMA * (v[0] * ct + v[1] * st) / spd 316 | ay = tauMA * (-v[0] * st + v[1] * ct) / spd 317 | else: 318 | ax = tauMA * v[0] / spd 319 | ay = tauMA * v[1] / spd 320 | v[0] += ax 321 | v[1] += ay 322 | 323 | On the other hand, if we want to compute the velocity as a result of an 324 | arbitrary :math:`\theta` then we would instead use 325 | 326 | .. code-block:: python 327 | 328 | def fme_vel_gen(v, theta, L, tauMA): 329 | spd = math.hypot(v[0], v[1]) 330 | ct = math.cos(theta) 331 | mu = L - spd * ct 332 | if mu < 0: 333 | return 334 | if tauMA < mu: 335 | mu = tauMA 336 | st = math.sin(theta) 337 | ax = mu * (v[0] * ct + v[1] * st) / spd 338 | ay = mu * (-v[0] * st + v[1] * ct) / spd 339 | v[0] += ax 340 | v[1] += ay 341 | 342 | Note that these two implementations will no work if the speed is zero. This is 343 | a feature and not a bug: when the speed is zero the direction is undefined. In 344 | other words, the meaning of "rightstrafe" or "leftstrafe" will be lost without 345 | specifying additional information. 346 | 347 | For backpedalling, we have 348 | 349 | .. code-block:: python 350 | 351 | def fme_spd_back(spd, L, tauMA): 352 | return abs(spd - min(tauMA, L + spd)) 353 | 354 | Then we have the following function which applies friction. This function must 355 | be called before calling the speed functions when groundstrafing. 356 | 357 | .. code-block:: python 358 | 359 | def apply_fric(spd, E, ktau): 360 | if spd > E: 361 | return spd * (1 - ktau) 362 | tmp = E * ktau 363 | if spd > tmp: 364 | return spd - tmp 365 | return 0 366 | -------------------------------------------------------------------------------- /doc/basicphy.rst: -------------------------------------------------------------------------------- 1 | .. highlight:: none 2 | 3 | Basic physics 4 | ============= 5 | 6 | 7 | Walking through a frame 8 | ----------------------- 9 | 10 | .. caution:: 11 | This section is incomplete and outdated. 12 | 13 | In this section we will describe what the game does in each frame that are 14 | relevant to TASing. Do not skip this section if you intend to understand the 15 | rest of this document without much pain. 16 | 17 | #. For our purposes we can consider ``PlayerPreThink`` in ``dlls/client.cpp`` 18 | to be the first relevant function that gets called in each frame. You might 19 | be wondering about the ``StartFrame`` function. While usually it seems to 20 | get called prior to ``PlayerPreThink``, inexplicable things happen when one 21 | tries to output ``g_ulFrameCount`` and ``gpGlobals->time`` from both 22 | ``StartFrame`` and ``PlayerPreThink``. What you will observe in 1000 fps is 23 | something like the following:: 24 | 25 | startframe, g_ulFrameCount = 3, gpGlobals->time = 1.001 26 | prethink, g_ulFrameCount = 3, gpGlobals->time = 1.003 27 | startframe, g_ulFrameCount = 4, gpGlobals->time = 1.002 28 | prethink, g_ulFrameCount = 4, gpGlobals->time = 1.004 29 | ... 30 | ... 31 | 32 | If we look only at ``g_ulFrameCount``, it appears that ``StartFrame`` is 33 | indeed called prior to ``PlayerPreThink``. However, the values for 34 | ``gpGlobals->time`` seem to tell us that the invocation of ``StartFrame`` is 35 | delayed by 2 frames. We have no idea how to explain this contradiction, 36 | considering the fact that ``gpGlobals`` is a global pointer referencing the 37 | same memory location. There could be some evil code that changes the value 38 | of ``gpGlobals->time`` to deceive ``StartFrame``. Furthermore, when the 39 | frametime (the inverse of framerate, hereafter denoted as :math:`\tau`) is 40 | not a multiple of 0.001, sometimes ``StartFrame`` will be called multiple 41 | times in succession without executing the game physics at all, followed by 42 | several iterations of the game physics in sucession without calling 43 | ``StartFrame``. Also, while a map is loading, the entire game physics is 44 | run multiple times after each ``StartFrame`` call. From these observations, 45 | we conclude that one "frame" really refers to one iteration of the game 46 | physics, which can be thought to begin with the call to ``PlayerPreThink`` 47 | as far as player physics is concerned, rather than ``StartFrame``. 48 | 49 | #. ``PlayerPreThink`` will call ``CBasePlayer::PreThink`` located in 50 | ``dlls/player.cpp``. Information such as the player health and those 51 | related to HUD will be sent over to the client in a function called 52 | ``CBasePlayer::UpdateClientData``. Physics such as the using of trains, 53 | jumping animations, sounds and other cosmetic effects are run. The physics 54 | associated with other entities in the map are not run at this point. 55 | 56 | #. The ``UpdateClientData``, which is a completely different function located 57 | in ``dlls/client.cpp`` will get called a short while later. This is where 58 | TasTools send the player velocity, position and other data in full precision 59 | to the clientside. This is important because of lines like these in the 60 | ``delta.lst`` file located in the ``valve`` directory:: 61 | 62 | DEFINE_DELTA( velocity[0], DT_SIGNED | DT_FLOAT, 16, 8.0 ), 63 | DEFINE_DELTA( velocity[1], DT_SIGNED | DT_FLOAT, 16, 8.0 ), 64 | ... 65 | ... 66 | DEFINE_DELTA( velocity[2], DT_SIGNED | DT_FLOAT, 16, 8.0 ), 67 | 68 | This means that each component of player velocity will be sent only with 16 69 | bits of precision and rounded to a multiple of one-eighth. This is bad for 70 | optimal strafing computations done entirely at the clientside. 71 | 72 | #. Assuming the string ``CL_SignonReply: 2`` has been printed to the console at 73 | some point (this will only be printed once). Now the client starts working. 74 | The messages sent from the server just a moment ago are now received, 75 | including one that was sent by TasTools from ``UpdateClientData``. Once the 76 | messages are processed, the game calls ``CL_CreateMove`` in 77 | ``cl_dlls/input.cpp``. This is where the user input gets processed. The 78 | user input can come from keyboard, mouse, joystick, or just console commands 79 | such as ``+attack``. This is also where TasTools does all TAS-related 80 | computations such as optimal strafing. The final viewangles, button presses 81 | (represented by a 16-bit integer) and other data are sent to the server at 82 | some point after ``CL_CreateMove`` returns, stored as ``usercmd_t`` defined 83 | in ``common/usercmd.h``. Note that button presses do not record the actual 84 | keys pressed, but rather, they are represented by bits that are set when 85 | specific commands are issued. For example, when the ``+forward`` command is 86 | issued, the ``IN_FORWARD`` bit will be set. For some reasons, the game 87 | developers named the variable as ``buttons`` which is confusing. The 88 | definition for all "button" bits can be found in ``common/in_buttons.h``. 89 | 90 | #. Once the server receives the user input from the clientside, the ``PM_Move`` 91 | in ``pm_shared/pm_shared.c`` gets called. This is where all the fun begins, 92 | because the physics related to player movement are located here. Due to the 93 | importance of the code in ``pm_shared/pm_shared.c``, we will devote a couple 94 | of paragraphs to them. ``PM_Move`` calls ``PM_PlayerMove``, which is an 95 | unfortunately large function. 96 | 97 | #. The first thing ``PM_PlayerMove`` does is to call ``PM_CheckParamters`` 98 | [*sic*]. This is the function that scales ``forwardmove``, ``sidemove`` and 99 | ``upmove`` (hereafter written as :math:`F`, :math:`S` and :math:`U` 100 | respectively) in ``pmove->cmd`` so that the magnitude of the vector 101 | :math:`\langle F,S,U \rangle` does not exceed ``sv_maxspeed`` (denoted as 102 | :math:`M_m`). This means if :math:`\lVert\langle F,S,U\rangle\rVert > M_m` 103 | then 104 | 105 | .. math:: \langle F,S,U\rangle \mapsto \frac{M_m}{\lVert\langle F,S,U\rangle\rVert} \langle F,S,U\rangle 106 | 107 | Otherwise, :math:`F`, :math:`S` and :math:`U` will remain unchanged. This 108 | is why increasing ``cl_forwardspeed``, ``cl_sidespeed`` and ``cl_upspeed`` 109 | may not result in greater acceleration. ``PM_DropPunchAngle`` is also 110 | invoked to decrease the magnitude of punchangles. The full equation is 111 | 112 | .. math:: \mathbf{P}' = \max\left[ \lVert\mathbf{P}\rVert \left( 1 - \frac{1}{2} \tau \right) - 10\tau, 0 \right] \mathbf{\hat{P}} 113 | 114 | where :math:`\mathbf{P} = \langle P_p, P_y, P_r\rangle`. The punchangles 115 | are then added to the viewangles (which does not affect the viewangles at 116 | the clientside). ``PM_ReduceTimers`` is now called to decrement various 117 | timers such as ``pmove->flDuckTime`` which purpose will be explained later. 118 | 119 | #. A very important function, ``PM_CatagorizePosition`` [*sic*], is then 120 | called. Here we will introduce a new concept: onground. A player is said 121 | to be onground if the game thinks that the player is standing some "ground" 122 | so that certain aspects of the player physics will be different from that 123 | when the player is not onground. ``PM_CatagorizePosition`` checks whether 124 | the player is onground and also determines the player's waterlevel (which 125 | carries a numerical value), yet another new concept. In short, the player 126 | physics will differ significantly only when the waterlevel is more than 1, 127 | which happens when the player is immersed sufficiently deeply into some 128 | water (the specific conditions will be described later). 129 | 130 | The way by which ``PM_CatagorizePosition`` determins the player onground 131 | status is simple. A player is onground if :math:`v_z` (vertical velocity) 132 | is at most 180 ups, *and* if there exists a plane at most 2 units below the 133 | player, such that the angle between the plane and the global horizontal 134 | :math:`xy` plane is at most :math:`\arccos 0.7 \approx 45.57` degrees with 135 | the plane normal pointing upward (this is another way of saying that the 136 | :math:`z` component of the unit normal vector is at least 0.7). If the 137 | player is determined to be onground and his waterlevel is less than 2, then 138 | the game will forcibly shift the player position downward so that the player 139 | is really standing on the plane and not continue floating in the air at most 140 | 2 units above the plane. 141 | 142 | TODO describe checkwater here 143 | 144 | #. After the first onground check, the game will store :math:`-v_z` in 145 | ``pmove->flFallVelocity``. Although this may seem insignificant, this 146 | turned out to be how the game calculates fall damage. Next we have a call 147 | to ``PM_Ladder``, which determines whether the player is on some ladder. 148 | 149 | #. ``PM_Duck``, as its name suggests, is pretty important. This is the 150 | function responsible for ducking physics. Here we must introduce two new 151 | concepts: bounding box and duckstate. They are described in 152 | :ref:`ducking-phy` which must be read before moving on. 153 | 154 | #. The game will now call ``PM_LadderMove`` if the player is on some ladder. 155 | The ladder physics is described in :ref:`ladder-phy`. 156 | 157 | #. If ``+use`` and the player is onground, then :math:`\mathbf{v}` will be 158 | scaled down by 0.3. This is the basis of USE braking. 159 | 160 | #. The game will now do different things depending on ``pmove->movetype``. If 161 | the player is on ladder then the movetype is ``MOVETYPE_FLY``. Otherwise it 162 | will usually be the confusingly named ``MOVETYPE_WALK``. We will assume the 163 | latter. If the player waterlevel is at most 1, the game makes the first 164 | gravity computation as done by ``PM_AddCorrectGravity``. Looking inside 165 | this function, assuming basevelocity is :math:`\mathbf{0}` we see that the 166 | game performs this following computation: 167 | 168 | .. math:: v_z' = v_z - \frac{1}{2} g\tau 169 | 170 | where :math:`g` is the gravitational acceleration, ``ent_gravity`` times 171 | ``pmove->movevars->gravity``. Several notes must be made here: 172 | ``ent_gravity`` is ``pmove->gravity`` if the latter is nonzero, otherwise 173 | the former is 1. ``pmove->gravity`` is usually 1, which can thought as a 174 | multiplier that scales ``pmove->movevars->gravity``. For instance, it has a 175 | different value if we enter the Xen maps, which is how the game changes the 176 | gravitational acceleration without directly modifying the ``sv_gravity`` 177 | cvar. Now look closer to the computation, we see that it does seem 178 | incorrect as noted in the rather unhelpful comment. However, the game 179 | always makes a call to ``PM_FixupGravityVelocity`` towards the end of 180 | ``PM_PlayerMove`` which performs the exact same computation except it 181 | completely ignores basevelocity. Now the key idea is that the actual 182 | movement of player vertical position is done between these two calls. In 183 | other words, we want the final vertical position after ``PM_PlayerMove`` to 184 | be 185 | 186 | .. math:: p_z' = p_z + v_z \tau - \frac{1}{2} g\tau^2 187 | 188 | which is exactly what we know from classical mechanics. But by rewriting 189 | this equation ever so slightly, we obtain 190 | 191 | .. math:: p_z' = p_z + \tau \left( v_z - \frac{1}{2} g\tau \right) = p_z + \tau v_z' 192 | 193 | where :math:`v_z'` is the new velocity computed by ``PM_AddCorrectGravity``. 194 | It is now obvious why this function does it in the seemingly incorrect way. 195 | 196 | The final velocity after ``PM_PlayerMove`` must be :math:`v_z - g\tau` and 197 | *not* :math:`v_z - \frac{1}{2} g\tau`. This is where 198 | ``PM_FixupGravityVelocity`` comes into play by subtracting another 199 | :math:`\frac{1}{2} g\tau`. A final note: in both of these functions the 200 | ``PM_CheckVelocity`` is called. This function ensures each component of 201 | :math:`\mathbf{v}` is clamped to ``sv_maxvelocity``. 202 | 203 | #. TODO: waterjump etc 204 | 205 | #. Assuming the waterlevel is less than 2. If ``+jump`` is active then 206 | ``PM_Jump`` will be called. The jumping physics will be dealt in a later 207 | section. 208 | 209 | #. The game calls ``PM_Friction`` which reduces the magnitude of player 210 | horizontal velocity if the player is onground. The friction physics is 211 | discussed much later. Note that the vertical speed is zeroed out here. 212 | Another ``PM_CheckVelocity`` will be called regardless of onground status. 213 | 214 | #. The game will now perform the main movement physics. They are very 215 | intricate and we devoted several sections to them. 216 | 217 | #. The final ``PM_CatagorizePosition`` will be called after the movement 218 | physics. The basevelocity will be subtracted away from the player velocity, 219 | followed by yet another ``PM_CheckVelocity``. Then the 220 | ``PM_FixupGravityVelocity`` is called. Finally, if the player is onground 221 | the vertical velocity will be zeroed out again. 222 | 223 | #. After ``PM_Move`` returns, the game will call ``CBasePlayer::PostThink`` 224 | shortly after. This is where fall damage is inflicted upon the player (only 225 | if the player is onground at this point), impulse commands are executed, 226 | various timers and counters are decremented, and usable objects in vicinity 227 | will be used if ``+use``. 228 | 229 | #. The game will now execute the ``Think`` functions for all entities. This is 230 | also where other damages and object boosting are handled. 231 | 232 | 233 | Frame rate 234 | ---------- 235 | 236 | The term "frame rate" is potentially ambiguous. If precision is desirable then 237 | we can differentiate between three kinds of frame rate: computational frame 238 | rate (CFR), usercmd frame rate (UFR) and rendering frame rate (RFR). The RFR 239 | is simply the rate at which frames are drawn on the screen. (Note that it is 240 | incorrect to define RFR as the "number of screen refreshes per second". This 241 | definition falls apart if ``r_norefresh 1``, which prevents the screen from 242 | refreshing!) 243 | 244 | TODO!! 245 | 246 | 247 | .. _ducking-phy: 248 | 249 | Ducking physics 250 | --------------- 251 | 252 | The bounding box is an imaginary cuboid, usually enclosing the player model. 253 | It is sometimes called the AABB, which stands for axis-aligned bounding box, 254 | which means the edges of the box are always parallel to the corresponding axes, 255 | regardless of player position and orientation. The bounding box is used for 256 | collision detection. If a solid entity touches this bounding box, then this 257 | entity is considered to be in contact with the player, even though it may not 258 | actually intersect the player model. The height of the player bounding box can 259 | change depending on the duckstate. 260 | 261 | When the player is not ducking, we say the player is unducked and thus the 262 | duckstate is 0. In this state the bounding box has width 32 units and height 263 | 72 units. If the duck key has been held down for no longer than 1 second and 264 | the player has been onground all the while, then we say that the player 265 | duckstate is 1. At this point the bounding box height remains the same as that 266 | when the player has duckstate of 0. If the duck key is held (regardless of 267 | duration) but not onground, or if the duck key has been held down for more than 268 | 1 second while onground, the duckstate will be 2. Now the bounding box height 269 | will be 36 units, with the same width as before. 270 | 271 | If the duck key is released while the player duckstate is 2, the duckstate will 272 | be changed back to 0 immediately, and the bounding box height will switch back 273 | to 72 units. However, if the key is released while the duckstate is 1, magic 274 | will happen: the player position will be shifted instantaneously 18 units above 275 | the original position, provided that there are sufficient empty space above the 276 | player. This forms the basis of ducktapping, sometimes referred to as the 277 | imprecise name "doubleduck". Doubleduck is really a ducktap followed by 278 | another duck. 279 | 280 | Note that the bounding box is an actual concept in the game code, while 281 | duckstate is simply an easier abstraction used in our literature. In the code, 282 | a duckstate of 0 means ``pmove->bInDuck == false`` and ``(pmove->flags & 283 | FL_DUCKING) == 0``. A duckstate of 1 means ``pmove->bInDuck == true`` and 284 | ``(pmove->flags & FL_DUCKING) == 0`` still. Finally, a duckstate of 2 means 285 | ``pmove->bInDuck == false`` and ``(pmove->flags & FL_DUCKING) != 0``. Whereas 286 | the type of bounding box is essentially selected by modifying 287 | ``pmove->usehull``. 288 | 289 | Now that we have described the concept of bounding box and duckstate, we will 290 | now note that each of :math:`F`, :math:`S` and :math:`U` will be scaled down by 291 | 0.333 if the duckstate is 2. After the scaling down, :math:`\lVert\langle 292 | F,S,U\rangle\rVert` becomes :math:`0.333M`, ignoring floating point errors and 293 | assuming original :math:`\lVert\langle F,S,U\rangle\rVert \ge M`. However, 294 | this is done *before* any change in duckstate happens in ``PM_Duck``. Suppose 295 | the player has duckstate 0 before the call to ``PM_Duck``, and after 296 | ``PM_Duck`` is called the duckstate changes to 2. In this case, the 297 | multiplication by 0.333 will *not* happen: the duckstate was not 2 before the 298 | change. Suppose the player has duckstate 2 and the call to ``PM_Duck`` makes 299 | the player unducks, hence changing the duckstate back to 0. In this case the 300 | multiplication *will* happen. 301 | 302 | 303 | Jumping physics 304 | --------------- 305 | 306 | Assuming the player is ongorund. Then jumping is possible only if he is 307 | onground and the ``IN_JUMP`` bit is unset in ``pmove->oldbuttons``. 308 | 309 | 310 | Jumpbug 311 | ------- 312 | 313 | Jumpbug is one of a few exploits that can bypass fall damage when landing on 314 | any ground. The downside of jumpbug is that a jump must be made, which may be 315 | undesirable under certain circumstances. For example, when the player jumps 316 | the bunnyhop cap will be triggered. 317 | 318 | .. image:: _static/jumpbug.png 319 | 320 | To begin a jumpbug sequence, suppose that the player is initially not onground 321 | (as determined by the first onground check) and that the duckstate is 2, as 322 | illustrated by the ``+duck`` bounding box in the figure above. Some time later 323 | the player unducks, hence ``PM_UnDuck`` will be called to change the duckstate 324 | back to 0 and the second onground check will be triggered. If there exists a 325 | ground 2 units below the player, then the player will now be onground (as shown 326 | by the ``-duck`` box above), and if ``+jump`` happens to be active the player 327 | will jump when ``PM_Jump`` is called within the same frame (shown by the 328 | ``+jump`` box). But recall that ``PM_Jump`` will always make the player to be 329 | not onground. Also, as the upward velocity is now greater than 180 ups, when 330 | the third onground check is made the player will again be determined to be not 331 | onground. As a result, when the control passes to ``CBasePlayer::PostThink``, 332 | the game will not inflict fall damage to the player. 333 | 334 | Jumpbug can fail if the player was not able to unduck to make himself onground 335 | after the second groundcheck. The chances of this happening is greater at 336 | lower frame rates and higher vertical speeds. 337 | 338 | 339 | Edgebug 340 | ------- 341 | 342 | TODO 343 | 344 | 345 | Basevelocity and pushfields 346 | --------------------------- 347 | 348 | In ``pm_shared.c`` the basevelocity is stored in ``pmove->basevelocity``. This 349 | is nonzero usually when being inside a ``trigger_push`` or conveyor belt, which 350 | are called *pushfields*. The way the player physics incorporates basevelocity 351 | is vastly different for its horizontal and vertical components. 352 | 353 | The basevelocity is always added to the player velocity *after* acceleration. 354 | This means the new player position is computed by :math:`\mathbf{p}' = 355 | \mathbf{p} + (\mathbf{v} + \mathbf{b}) \tau` where :math:`\mathbf{b}` is the 356 | basevelocity. Shortly after that the basevelocity will be subtracted away from 357 | :math:`\mathbf{v}` before ``PM_PlayerMove`` returns. 358 | 359 | Interestingly, whenever the player leaves a pushfield the basevelocity of the 360 | pushfield will be added to the player's velocity somewhere in the game engine. 361 | The added components will not be subtracted away. This is the basis of the 362 | famous push trigger boost, whereby a player ducks and unducks in rapid 363 | succession so that the bounding box enters and leaves the pushfield repeatedly. 364 | 365 | The :math:`b_z` is handled differently. It is incorperated into :math:`v_z` in 366 | ``PM_AddCorrectGravity`` without being subtracted away later. Instead, 367 | :math:`b_z` is set to zero in the function. Let us write :math:`v` to mean 368 | :math:`v_z` for now. The vertical velocity at the :math:`n`-th frame would be 369 | :math:`v_n = v_0 + (b - g) n\tau`. But bear in mind that the position is 370 | computed using :math:`v_n = v_0 + (b - g) n\tau + \frac{1}{2} g\tau` instead. 371 | Therefore, to find the position at an arbitrary :math:`n`\ -th frame we must 372 | compute 373 | 374 | .. math:: p_n = p_0 + \tau \sum_{k = 0}^n v_n = p_0 + \left( v_0 + \frac{1}{2} b\tau \right) n\tau + \frac{1}{2} n^2 \tau^2 (b - g) 375 | 376 | These formulae can be useful in planning. 377 | 378 | 379 | Water physics 380 | ------------- 381 | 382 | Water movement is unfortunately not optimisable in Half-Life. However, we will 383 | still include a description of its physics here. 384 | 385 | If the point 1 unit above the bottom of bounding box is immersed in water, then 386 | the waterlevel is 1. If the player origin (centre of bounding box) is 387 | additionally in water, then the waterlevel will be increased to 2. If the 388 | player's view (origin plus view offset) is also in water, then the waterlevel 389 | will be 3. Depending on the existence of water current and the waterlevel, the 390 | magnitude of basevelocity may be modified. 391 | 392 | In water physics the acceleration vector is :math:`\mathbf{a} = 393 | F\mathbf{\hat{f}} + S\mathbf{\hat{s}} + \langle 0,0,U\rangle` provided at least 394 | one of :math:`F`, :math:`S`, :math:`U` is nonzero. Otherwise :math:`\mathbf{a} 395 | = \langle 0,0,-60\rangle`. Note that :math:`\mathbf{a}` is an 396 | :math:`\mathbb{R}^3` vector. In the context of water physics we denote 397 | :math:`M = 0.8\min\left( M_m, \lVert\mathbf{a}\rVert \right)`, where it can be 398 | shown that, if not all :math:`F`, :math:`S`, :math:`U` are zero, then 399 | 400 | .. math:: \lVert\mathbf{a}\rVert = \sqrt{F^2 + S^2 + U^2 + \langle 0,0,2U\rangle 401 | \cdot \left( F\mathbf{\hat{f}} + S\mathbf{\hat{s}} \right)} 402 | 403 | Thus the water movement equation can be written as 404 | 405 | .. math:: \mathbf{v}' = \mathbf{v}(1 - k\tau) + \mu\mathbf{\hat{a}} 406 | 407 | with 408 | 409 | .. math:: \mu = 410 | \begin{cases} 411 | \min(\gamma_1, \gamma_2) & \text{if } \gamma_2 > 0 \\ 412 | 0 & \text{otherwise} 413 | \end{cases} 414 | \quad\quad 415 | \gamma_1 = k\tau MA_g 416 | \quad\quad 417 | \gamma_2 = M - \lVert\mathbf{v}\rVert (1 - k\tau) 418 | 419 | The first thing we should notice is that :math:`\gamma_2` is independent of 420 | :math:`\mathbf{\hat{a}}`, which means as the speed increases :math:`\gamma_2` 421 | will inevitably decrease until it is negative. The speed can be written as 422 | 423 | .. math:: \lVert\mathbf{v}'\rVert = \sqrt{\lVert\mathbf{v}\rVert^2 (1 - k\tau)^2 + 424 | \mu^2 + 2\lVert\mathbf{v}\rVert (1 - k\tau) \mu \cos\theta} 425 | 426 | If :math:`\theta = 0` and :math:`\lVert\mathbf{v}\rVert` is sufficiently high 427 | so that :math:`\gamma_2 < \gamma_1`, then we see that 428 | :math:`\lVert\mathbf{v}'\rVert = M`. This means the maximum possible swimming 429 | speed is simply :math:`0.8M_m`. Moreover, assuming :math:`\mu = \gamma_1` then 430 | the acceleration is independent of frame rate: 431 | 432 | .. math:: \text{accel} = \frac{\left[ \lVert\mathbf{v}\rVert (1 - k\tau) + k\tau MA_g \right] - 433 | \lVert\mathbf{v}\rVert}{\tau} = k \left( MA_g - \lVert\mathbf{v}\rVert \right) 434 | 435 | Also observe that the player always experience geometric friction while in the 436 | water. 437 | -------------------------------------------------------------------------------- /doc/collisions.rst: -------------------------------------------------------------------------------- 1 | Collisions 2 | ---------- 3 | 4 | Many entities in Half-Life collide with one another. The velocity of the 5 | colliding entity usually changes as a result, while the position and velocity 6 | of the entity receiving the collision usually stay constant, countering real 7 | world Newtonian physics. The process of changing the velocity is usually 8 | referred to as *velocity clipping*. Collision is one of the most common events 9 | in Half-Life, so it is worthwhile to study its physics. 10 | 11 | Collision is detected by determining the planes that run into the way of a line 12 | traced from the moving entity's position to the future position. The future 13 | position depends on the frame rate, the velocity and the base velocity 14 | associated with the colliding entity. Let :math:`\mathbf{\hat{n}}` be the 15 | plane normal and let :math:`\mathbf{v}` be the velocity at the instant of 16 | collision. Let :math:`b` be the *bounce coefficient* which, under certain 17 | conditions, depends on ``sv_bounce`` (denoted as :math:`B`) and :math:`k_e` 18 | (see :ref:`friction`). The bounce coefficient controls how the velocity is 19 | reflected akin to a light ray. If :math:`\mathbf{v}'` is the velocity 20 | resulting from the collision, then the *general collision equation* (GCE) can 21 | be written as 22 | 23 | .. math:: \mathbf{v}' = \mathbf{v} - b (\mathbf{v} \cdot \mathbf{\hat{n}}) 24 | \mathbf{\hat{n}} 25 | 26 | Before we proceed, we must point out that this equation may be applied multiple 27 | times per frame. The functions responsible of actually displacing entities are 28 | ``SV_FlyMove`` for non-players and ``PM_FlyMove`` for players. These functions 29 | perform at most four aforementioned line tracing, each time potentially calling 30 | the velocity clipping function. 31 | 32 | In most cases, players have :math:`b = 1` because :math:`k_e = 1` and so is 33 | :math:`B`. In general, :math:`b` for players is computed by :math:`b = 1 + B 34 | (1 - k_e)`. The case of :math:`b \ne 1` is more common for other entities. 35 | For example, snarks have :math:`b = 3/2` and :math:`k_e = 1/2`. In general, if 36 | the movement type of an entity is designated as ``MOVETYPE_BOUNCE``, then 37 | :math:`b = 2 - k_e`. 38 | 39 | Care must be taken when :math:`b < 1`. To understand why, we first observe 40 | that :math:`\mathbf{v} \cdot \mathbf{\hat{n}} < 0`, because otherwise there 41 | would not be any collision events. With 42 | 43 | .. math:: \mathbf{v}' \cdot \mathbf{\hat{n}} = (1 - b) \mathbf{v} \cdot 44 | \mathbf{\hat{n}} 45 | 46 | we see that if :math:`b < 1` then the angle between the resultant velocity and 47 | the plane normal is obtuse. As a result, collisions will occur indefinitely 48 | with an increasing :math:`\mathbf{v}`. To prevent this, the game utilises a 49 | safeguard immediately after the line tracing process in the respective 50 | ``FlyMove`` functions to set :math:`\mathbf{v}' = \mathbf{0}`. 51 | 52 | Hence, assuming :math:`b \ge 1` we employ the following trick to quickly find 53 | :math:`\lVert\mathbf{v}'\rVert`: write :math:`\lVert\mathbf{v}'\rVert^2 = 54 | \mathbf{v}' \cdot \mathbf{v}'` and expanding each :math:`\mathbf{v}'` in the 55 | RHS to give 56 | 57 | .. math:: \lVert\mathbf{v}'\rVert = \lVert\mathbf{v}\rVert \sqrt{1 - b(2 - b) 58 | \cos^2 \alpha} 59 | 60 | where :math:`\alpha` is the *smallest* angle between :math:`\mathbf{v}` and 61 | :math:`\mathbf{\hat{n}}` confined to :math:`[-\pi/2, \pi/2]`. Observe that the 62 | resulting speed is strictly increasing with respect to :math:`b` in :math:`[1, 63 | \infty)`. In fact, the curve of resultant speed against :math:`b` is 64 | hyperbolic provided :math:`\alpha \ne 0` and :math:`\alpha \ne \pm\pi/2`. When 65 | :math:`\alpha` does equal zero, the resultant speed will be linear in :math:`b` 66 | like so: 67 | 68 | .. math:: \lVert\mathbf{v}'\rVert = \lVert\mathbf{v}\rVert (b - 1) 69 | 70 | Again, this result assumes :math:`b \ge 1`. On the other hand, for the very 71 | common case of :math:`b = 1` we have 72 | 73 | .. math:: \lVert\mathbf{v}'\rVert = \lVert\mathbf{v}\rVert \, 74 | \lvert\sin\alpha\rvert 75 | 76 | Observe that the resultant velocity is always parallel to the plane, as one can 77 | verify that :math:`\mathbf{v}' \cdot \mathbf{\hat{n}} = 0` is indeed true. 78 | 79 | Speed preserving circular walls 80 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 81 | 82 | In Half-Life we can sometimes find concave walls made out of multiple planes to 83 | approximate an arc. Examples can be found in the ``c2a1`` map. Circular walls 84 | can be a blessing for speedrunners because they allow making sharp turns 85 | without losing too much speed. In fact, if the number of planes increases, the 86 | approximation will improve, and so the speed loss will decrease. Let :math:`n` 87 | be the number of walls and let :math:`\beta` be the angle subtended by the arc 88 | joining the midpoints of every wall. For example, with :math:`\beta = \pi/2` 89 | the first and the last walls will be perpendicular, and with :math:`\beta = 90 | \pi` the they will be opposite and parallel instead. Let :math:`\mathbf{v}_i` 91 | be the velocity immediatley after colliding with the :math:`i`-th wall, and 92 | assuming :math:`\mathbf{v}_0` is parallel to and coincident with the first 93 | wall. Assume also that :math:`0 \le \beta / (n-1) \le \pi/2`, which means that 94 | the angle between adjacent planes cannot be acute. If the velocity does not 95 | change due to other external factors throughout the collisions, then 96 | 97 | .. math:: \lVert\mathbf{v}_{i+1}\rVert = \lVert\mathbf{v}_i\rVert \cos \left( 98 | \frac{\beta}{n - 1} \right) 99 | 100 | The general equation is simply 101 | 102 | .. math:: \lVert\mathbf{v}_n\rVert = \lVert\mathbf{v}_0\rVert \cos^{n-1} \left( 103 | \frac{\beta}{n-1} \right) 104 | 105 | It can be verified that :math:`\lim_{n \to \infty} \lVert\mathbf{v}_n\rVert = 106 | \lVert\mathbf{v}_0\rVert`, hence the speed preserving property of circular 107 | walls. Observe also that the final speed is completely independent of the 108 | radius of the arc. Perfectly circular walls are impossible in Half-Life due to 109 | the inherent limitations in the map format, so some amount of speed loss is 110 | unavoidable. Nevertheless, even with :math:`n = 3` and :math:`\beta = \pi/2` 111 | we can still preserve half of the original speed. 112 | -------------------------------------------------------------------------------- /doc/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # TasTools documentation build configuration file, created by 5 | # sphinx-quickstart on Mon May 12 17:03:30 2014. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | import sys 17 | import os 18 | import sphinx_rtd_theme 19 | 20 | # If extensions (or modules to document with autodoc) are in another directory, 21 | # add these directories to sys.path here. If the directory is relative to the 22 | # documentation root, use os.path.abspath to make it absolute, like shown here. 23 | #sys.path.insert(0, os.path.abspath('.')) 24 | 25 | # -- General configuration ------------------------------------------------ 26 | 27 | # If your documentation needs a minimal Sphinx version, state it here. 28 | #needs_sphinx = '1.0' 29 | 30 | # Add any Sphinx extension module names here, as strings. They can be 31 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 32 | # ones. 33 | extensions = [ 34 | 'sphinx.ext.mathjax', 35 | ] 36 | 37 | # Add any paths that contain templates here, relative to this directory. 38 | templates_path = ['_templates'] 39 | 40 | # The suffix of source filenames. 41 | source_suffix = '.rst' 42 | 43 | # The encoding of source files. 44 | #source_encoding = 'utf-8-sig' 45 | 46 | # The master toctree document. 47 | master_doc = 'index' 48 | 49 | # General information about the project. 50 | project = 'Half-Life Physics' 51 | copyright = '2014-2015, Chong Jiang Wei' 52 | 53 | # The version info for the project you're documenting, acts as replacement for 54 | # |version| and |release|, also used in various other places throughout the 55 | # built documents. 56 | # 57 | # The short X.Y version. 58 | version = '' 59 | # The full version, including alpha/beta/rc tags. 60 | release = '' 61 | 62 | # The language for content autogenerated by Sphinx. Refer to documentation 63 | # for a list of supported languages. 64 | #language = None 65 | 66 | # There are two options for replacing |today|: either, you set today to some 67 | # non-false value, then it is used: 68 | #today = '' 69 | # Else, today_fmt is used as the format for a strftime call. 70 | #today_fmt = '%B %d, %Y' 71 | 72 | # List of patterns, relative to source directory, that match files and 73 | # directories to ignore when looking for source files. 74 | exclude_patterns = ['_build'] 75 | 76 | # The reST default role (used for this markup: `text`) to use for all 77 | # documents. 78 | #default_role = None 79 | 80 | # If true, '()' will be appended to :func: etc. cross-reference text. 81 | #add_function_parentheses = True 82 | 83 | # If true, the current module name will be prepended to all description 84 | # unit titles (such as .. function::). 85 | #add_module_names = True 86 | 87 | # If true, sectionauthor and moduleauthor directives will be shown in the 88 | # output. They are ignored by default. 89 | #show_authors = False 90 | 91 | # The name of the Pygments (syntax highlighting) style to use. 92 | pygments_style = 'sphinx' 93 | 94 | # A list of ignored prefixes for module index sorting. 95 | #modindex_common_prefix = [] 96 | 97 | # If true, keep warnings as "system message" paragraphs in the built documents. 98 | #keep_warnings = False 99 | 100 | 101 | # -- Options for HTML output ---------------------------------------------- 102 | 103 | # The theme to use for HTML and HTML Help pages. See the documentation for 104 | # a list of builtin themes. 105 | html_theme = 'sphinx_rtd_theme' 106 | 107 | # Theme options are theme-specific and customize the look and feel of a theme 108 | # further. For a list of options available for each theme, see the 109 | # documentation. 110 | #html_theme_options = {} 111 | 112 | # Add any paths that contain custom themes here, relative to this directory. 113 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 114 | 115 | # The name for this set of Sphinx documents. If None, it defaults to 116 | # " v documentation". 117 | html_title = 'Half-Life Physics Reference' 118 | 119 | # A shorter title for the navigation bar. Default is the same as html_title. 120 | #html_short_title = None 121 | 122 | # The name of an image file (relative to this directory) to place at the top 123 | # of the sidebar. 124 | #html_logo = None 125 | 126 | # The name of an image file (within the static path) to use as favicon of the 127 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 128 | # pixels large. 129 | #html_favicon = None 130 | 131 | # Add any paths that contain custom static files (such as style sheets) here, 132 | # relative to this directory. They are copied after the builtin static files, 133 | # so a file named "default.css" will overwrite the builtin "default.css". 134 | html_static_path = ['_static'] 135 | 136 | # Add any extra paths that contain custom files (such as robots.txt or 137 | # .htaccess) here, relative to this directory. These files are copied 138 | # directly to the root of the documentation. 139 | #html_extra_path = [] 140 | 141 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 142 | # using the given strftime format. 143 | html_last_updated_fmt = '%b %d, %Y' 144 | 145 | # If true, SmartyPants will be used to convert quotes and dashes to 146 | # typographically correct entities. 147 | html_use_smartypants = True 148 | 149 | # Custom sidebar templates, maps document names to template names. 150 | #html_sidebars = {} 151 | 152 | # Additional templates that should be rendered to pages, maps page names to 153 | # template names. 154 | #html_additional_pages = {} 155 | 156 | # If false, no module index is generated. 157 | html_domain_indices = False 158 | 159 | # If false, no index is generated. 160 | html_use_index = False 161 | 162 | # If true, the index is split into individual pages for each letter. 163 | #html_split_index = False 164 | 165 | # If true, links to the reST sources are added to the pages. 166 | html_show_sourcelink = False 167 | 168 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 169 | #html_show_sphinx = True 170 | 171 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 172 | #html_show_copyright = True 173 | 174 | # If true, an OpenSearch description file will be output, and all pages will 175 | # contain a tag referring to it. The value of this option must be the 176 | # base URL from which the finished HTML is served. 177 | #html_use_opensearch = '' 178 | 179 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 180 | #html_file_suffix = None 181 | 182 | # Output file base name for HTML help builder. 183 | htmlhelp_basename = 'HalfLifePhy' 184 | 185 | mathjax_path = 'https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS_HTML' 186 | 187 | 188 | # -- Options for LaTeX output --------------------------------------------- 189 | 190 | latex_elements = { 191 | # The paper size ('letterpaper' or 'a4paper'). 192 | #'papersize': 'letterpaper', 193 | 194 | # The font size ('10pt', '11pt' or '12pt'). 195 | #'pointsize': '10pt', 196 | 197 | # Additional stuff for the LaTeX preamble. 198 | #'preamble': '', 199 | } 200 | 201 | # Grouping the document tree into LaTeX files. List of tuples 202 | # (source start file, target name, title, 203 | # author, documentclass [howto, manual, or own class]). 204 | latex_documents = [ 205 | ('index', 'TasTools.tex', 'Half-Life Physics Documentation', 206 | 'Matherunner', 'manual'), 207 | ] 208 | 209 | # The name of an image file (relative to this directory) to place at the top of 210 | # the title page. 211 | #latex_logo = None 212 | 213 | # For "manual" documents, if this is true, then toplevel headings are parts, 214 | # not chapters. 215 | #latex_use_parts = False 216 | 217 | # If true, show page references after internal links. 218 | #latex_show_pagerefs = False 219 | 220 | # If true, show URL addresses after external links. 221 | #latex_show_urls = False 222 | 223 | # Documents to append as an appendix to all manuals. 224 | #latex_appendices = [] 225 | 226 | # If false, no module index is generated. 227 | #latex_domain_indices = True 228 | 229 | 230 | # -- Options for manual page output --------------------------------------- 231 | 232 | # One entry per manual page. List of tuples 233 | # (source start file, name, description, authors, manual section). 234 | man_pages = [ 235 | ('index', 'tastools', 'TasTools Documentation', 236 | ['Airstrafers'], 1) 237 | ] 238 | 239 | # If true, show URL addresses after external links. 240 | #man_show_urls = False 241 | 242 | 243 | # -- Options for Texinfo output ------------------------------------------- 244 | 245 | # Grouping the document tree into Texinfo files. List of tuples 246 | # (source start file, target name, title, author, 247 | # dir menu entry, description, category) 248 | texinfo_documents = [ 249 | ('index', 'TasTools', 'TasTools Documentation', 250 | 'Airstrafers', 'TasTools', 'One line description of project.', 251 | 'Miscellaneous'), 252 | ] 253 | 254 | # Documents to append as an appendix to all manuals. 255 | #texinfo_appendices = [] 256 | 257 | # If false, no module index is generated. 258 | #texinfo_domain_indices = True 259 | 260 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 261 | #texinfo_show_urls = 'footnote' 262 | 263 | # If true, do not generate a @detailmenu in the "Top" node's menu. 264 | #texinfo_no_detailmenu = False 265 | -------------------------------------------------------------------------------- /doc/explosions.rst: -------------------------------------------------------------------------------- 1 | Explosions 2 | ========== 3 | 4 | This page details the physics of explosion and explosive weapons, along with some speedrunning tricks that arise of out these aspects of the game. Familiarity with the health and damage system is assumed. 5 | 6 | .. _explosion physics: 7 | 8 | General physics 9 | --------------- 10 | 11 | An explosion is a phenomenon in Half-Life that inflicts damage onto surrounding entities. An explosion needs not be visible, though it is normally accompanied with a fiery visual effect. We may describe an explosion in terms of three fundamental properties. Namely, an explosion has an *origin*, a *source damage*, and a *radius*. 12 | 13 | Suppose an explosion occurs. Let :math:`D` be its source damage and :math:`R` its radius. Suppose there is an entity adjacent to the explosion origin. From gaming experience, we know that the further away this entity is from the explosion origin, the lower the damage inflicted on this entity. In fact, the game only looks for entities within a sphere of radius :math:`R` from the explosion origin, ignoring all entities beyond. In the implementation, this is achieved by calling ``UTIL_FindEntityInSphere`` with the radius as one of the parameters. 14 | 15 | Assume the entity in question is within :math:`R` units from the explosion origin. First, the game traces a line from the explosion origin to the entity's *body target*. Recall from :ref:`entities` that the body target of an entity is usually, but not always, coincident with the entity's origin. Then, the game computes the distance between this entity's body target and the explosion origin as :math:`\ell`. The damage inflicted onto this entity is thus computed to be 16 | 17 | .. math:: D \left( 1 - \frac{\ell}{R} \right) \qquad (0 \le \ell \le R) 18 | 19 | Observe that the damage inflicted falls off linearly with distance, and not with the square of distance as is the case in the real world. This process is repeated with other entities found within the sphere. 20 | 21 | Interestingly, the computed distance :math:`\ell` may not equal to the actual distance between this entity and explosion origin. In particular, if the line trace is startsolid, then the game computes :math:`\ell = 0`. As a result, the damage inflicted on the entity is exactly the source damage of the explosion. Indeed, all entities within the sphere will receive the same damage. 22 | 23 | The case where the line trace is startsolid is seemingly impossible to achieve. Fortunately, this edge case is not hard to exploit in game, the act of which is named *nuking* as will be detailed in :ref:`nuking`. The key to understanding how such exploits might work is to observe that the explosion origin may not coincide with the origin of the entity just before it detonates. The exact way the explosion origin is computed depends on the type of entity generating the explosion. 24 | 25 | Explosion origin 26 | ---------------- 27 | 28 | Explosions are always associated with a *source entity*. This entity could be a grenade (of which there are three kinds) or an ``env_explosion``. 29 | 30 | Denote :math:`\mathbf{r}` the position of the associated entity. When an explosion occurs, the game will trace a line from :math:`A` to :math:`B`. The exact coordinates of these two points depend on the type of the associated source entity, but they are always, in one way or the other, offset from the source entity's origin. In general, we call :math:`\mathbf{c}` the end position from the line trace. If the trace fraction is not 1, the game will modify the position of the source entity. Otherwise, the position will not be changed. 31 | 32 | The new position of the source entity is computed to be 33 | 34 | .. math:: \mathbf{r}' := \mathbf{c} + \frac{3}{5} (D - 24) \mathbf{\hat{n}} 35 | 36 | All numerical constants are hardcoded. Call the coefficient of :math:`\mathbf{\hat{n}}` the *pull out distance*, as per the comments in the implementation in ``ggrenade.cpp``. This is so named because if the source entity is a grenade, it is typically in contact with some plane or ground when it explodes. By modifying the origin this way, the source entity is being pulled out of the plane by that distance. Remarkably, this distance depends on the source damage of the explosion. For instance, MP5 grenades create explosions with a source damage of :math:`D = 100`, therefore MP5 grenades are pulled out of the plane by 45.6 units at detonation. 37 | 38 | Subsequently, the source entity will begin to properly explode. The physics driving the rest of this event has been described in :ref:`explosion physics`. Most importantly, the explosion origin is set to be :math:`\mathbf{r}' + \mathbf{\hat{k}}`. Observe how the :math:`\mathbf{\hat{k}}` is added to the entity's origin, the purpose of which is to pull non-contact grenades out of the ground slightly, as noted in the comments. In the implementation, the addition of this term is done in the function responsible for applying explosive damage, namely ``RadiusDamage``. Since all explosion code invoke this function, this term is always added to the origin for any explosion that happens. 39 | 40 | Contact grenades 41 | ~~~~~~~~~~~~~~~~ 42 | 43 | A contact grenade is a type of grenade which detonates upon contact with a solid entity. This includes the MP5 grenades and RPGs. 44 | 45 | Let :math:`\mathbf{r}` be the origin of a contact grenade moving in space. Assuming the map is closed, the grenade will eventually hit some entity and then detonate. Denote unit vector :math:`\mathbf{\hat{n}}` the normal to the plane on the entity that got hit. Note that at the instant the grenade collides with the plane, its position will be on the plane. Thus at this instant, let :math:`\mathbf{v}` be the velocity of the grenade. 46 | 47 | Then, the start and end points of the line trace are given by 48 | 49 | .. math:: 50 | \begin{align*} 51 | A &:= \mathbf{r} - 32 \mathbf{\hat{v}} \\ 52 | B &:= \mathbf{r} + 32 \mathbf{\hat{v}} 53 | \end{align*} 54 | 55 | Here, :math:`A` is 32 units away from the position of the grenade at collision, in the opposite direction of its velocity. And :math:`B` is 32 units away from that position, but in the direction of the velocity. It is easy to imagine that, more often than not, the end position of the line trace will coincide with the grenade position. This line trace will also rarely be startsolid. This is because the grenade has to pass through open space before hitting the plane, and :math:`A` is approximately one of the grenade's past positions. 56 | 57 | Timed grenades 58 | ~~~~~~~~~~~~~~ 59 | 60 | Timed grenades are grenades that detonate after a specific amount of time. This includes hand grenades, which explode three seconds after the pin is pulled. 61 | 62 | Denote :math:`\mathbf{r}` the origin of a timed grenade. At detonation, the grenade may or may not be lying on a plane. Since the grenade could well be resting on the ground with zero velocity, it does not make sense to use the velocity in computing the start and end points for the line trace. Instead, Valve decided to use :math:`\mathbf{\hat{k}}` to offset those points from the grenade origin. So, we have 63 | 64 | .. math:: 65 | \begin{align*} 66 | A &:= \mathbf{r} + 8 \mathbf{\hat{k}} \\ 67 | B &:= \mathbf{r} - 32 \mathbf{\hat{k}} 68 | \end{align*} 69 | 70 | Now, :math:`A` is simply 8 units above the grenade and :math:`B` is 32 units below the grenade. This means that there is a greater chance that this line trace is startsolid and also that the trace fraction is 1. The former can occur if there is a solid entity above the grenade, while the latter can occur if the grenade is sufficiently high above the ground. 71 | 72 | Explosions by ``env_explosion`` 73 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 74 | 75 | Tripmines 76 | ~~~~~~~~~ 77 | 78 | .. _nuking: 79 | 80 | Nuking 81 | ------ 82 | 83 | Nuking refers to the trick of placing explosives in locations confined in a particular way so as to disable damage falloff. The result is that, for all entities found within the sphere of radius :math:`R` from the explosion origin, the damage inflicted will be the maximum damage :math:`D`, effectively with :math:`\ell = 0`. The usefulness of this trick is obvious. 84 | 85 | It is important to keep in mind that the explosion radius does not change when nuking. Entities outside the sphere will remain untouched by the explosion. 86 | -------------------------------------------------------------------------------- /doc/fundamentals.rst: -------------------------------------------------------------------------------- 1 | Fundamentals 2 | ============ 3 | 4 | Notation 5 | -------- 6 | 7 | One of the most important mathematical objects in discussions of Half-Life physics is the Euclidean vector. All vectors are in either :math:`\mathbb{R}^2` or :math:`\mathbb{R}^3`, where :math:`\mathbb{R}` denotes the real numbers. This is sometimes not specified explicitly if the contextual clues are sufficient for disambiguation. 8 | 9 | All vectors are written in boldface like so: 10 | 11 | .. math:: \mathbf{v} 12 | 13 | Every vector has an associated length, which is referred to as the *norm*. The norm of some vector :math:`\mathbf{v}` is thus denoted as 14 | 15 | .. math:: \lVert\mathbf{v}\rVert 16 | 17 | A vector of length one is called a *unit vector*. So the unit vector in the direction of some vector :math:`\mathbf{v}` is written with a hat: 18 | 19 | .. math:: \mathbf{\hat{v}} = \frac{\mathbf{v}}{\lVert\mathbf{v}\rVert} 20 | 21 | There are three special unit vectors, namely 22 | 23 | 24 | .. math:: \mathbf{\hat{i}} \quad \mathbf{\hat{j}} \quad \mathbf{\hat{k}} 25 | 26 | These vectors point towards the positive :math:`x`, :math:`y` and :math:`z` axes respectively. 27 | 28 | Every vector also has components in each axis. For a vector in :math:`\mathbb{R}^2`, it has an :math:`x` component and a :math:`y` component. A vector in :math:`\mathbb{R}^3` has an additional :math:`z` component. To write out the components of a vector explicitly, we have 29 | 30 | .. math:: \mathbf{v} = \langle v_x, v_y, v_z\rangle 31 | 32 | This is equivalent to writing :math:`\mathbf{v} = v_x \mathbf{\hat{i}} + v_y \mathbf{\hat{j}} + v_z \mathbf{\hat{k}}`. However, we never write out the components this way in this documentation as it is tedious. Notice that we are writing vectors as row vectors. This will be important to keep in mind when we apply matrix transformations to vectors. 33 | 34 | The dot product between two vectors :math:`\mathbf{a}` and :math:`\mathbf{b}` is written as 35 | 36 | .. math:: \mathbf{a} \cdot \mathbf{b} 37 | 38 | On the other hand, the cross product between :math:`\mathbf{a}` and :math:`\mathbf{b}` is 39 | 40 | .. math:: \mathbf{a} \times \mathbf{b} 41 | 42 | Viewangles 43 | ---------- 44 | 45 | The term *viewangles* is usually associated with the player entity. The viewangles refer to a group of three angles which describe the player's view orientation. We call these angles *yaw*, *pitch* and *roll*. Mathematically, we denote the yaw by 46 | 47 | .. math:: \vartheta 48 | 49 | and the pitch by 50 | 51 | .. math:: \varphi 52 | 53 | Note that these are different from :math:`\theta` and :math:`\phi`. We do not have a mathematical symbol for roll as it is rarely used. In mathematical discussions, the viewangles are assumed to be in *radians* unless stated otherwise. However, do keep in mind that they are stored in degrees in the game. 54 | 55 | One way to change the yaw and pitch is by moving the mouse. This is not useful for tool-assisted speedrunning, however. A better method for precise control of the yaw and pitch angles is by issuing the commands ``+left``, ``+right``, ``+up``, or ``+down``. When these commands are active, the game increments or decrements the yaw or pitch by a certain controllable amount per frame. The amounts can be controlled by adjusting the variables ``cl_yawspeed`` and ``cl_pitchspeed``. For instance, when ``+right`` is active, the game multiplies the value of ``cl_yawspeed`` by the frame time, then subtracts the result from the yaw angle. 56 | 57 | View vectors 58 | ------------ 59 | 60 | There are two vectors associated with the player's viewangles. These are called the *view vectors*. For discussions in 3D space, they are defined to be 61 | 62 | .. math:: 63 | \begin{align*} 64 | \mathbf{\hat{f}} &:= \langle \cos\vartheta \cos\varphi, \sin\vartheta \cos\varphi, -\sin\varphi \rangle \\ 65 | \mathbf{\hat{s}} &:= \langle \sin\vartheta, -\cos\vartheta, 0 \rangle 66 | \end{align*} 67 | 68 | We will refer to the former as the *unit forward vector* and the latter as the *unit right vector*. The negative sign for :math:`f_z` is an idiosyncrasy of the GoldSrc engine inherited from Quake. This is the consequence of the fact that looking up gives negative pitch and vice versa. 69 | 70 | We sometimes restrict our discussions to the horizontal plane, such as in the description of strafing. In this case we assume :math:`\varphi = 0` and define 71 | 72 | .. math:: 73 | \begin{align*} 74 | \mathbf{\hat{f}} &:= \langle \cos\vartheta, \sin\vartheta \rangle \\ 75 | \mathbf{\hat{s}} &:= \langle \sin\vartheta, -\cos\vartheta \rangle 76 | \end{align*} 77 | 78 | Such restriction is equivalent to projecting the :math:`\mathbf{\hat{f}}` vector onto the :math:`xy` plane, provided the original vector is not vertical. 79 | 80 | The above definitions are not valid if the roll is nonzero. Nevertheless, such situations are extremely rare in practice. 81 | 82 | .. _entities: 83 | 84 | Entities 85 | -------- 86 | 87 | .. _tracing: 88 | 89 | Tracing 90 | ------- 91 | 92 | Tracing is one of the most important computations done by the game. Tracing is done countless times per frame, and it is vital to how entities interact with one another. -------------------------------------------------------------------------------- /doc/glossary.rst: -------------------------------------------------------------------------------- 1 | Glossary 2 | ======== 3 | 4 | .. glossary:: 5 | :sorted: 6 | 7 | explosion origin 8 | explosion radius 9 | An explosion may be modelled as a sphere in which entities receive damage. The explosion origin is then the centre of this sphere, and the explosion radius is its radius. 10 | 11 | viewangles 12 | A group of three angles comprising of yaw, pitch and roll. This is associated with every entity, representing the view orientation. 13 | 14 | unit acceleration vector 15 | The result of 16 | 17 | ducktapping 18 | One of the ground avoidance tricks which involves tapping the duck key while on the ground, resulting in the player popping out 18 units above the ground. 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /doc/index.rst: -------------------------------------------------------------------------------- 1 | Half-Life Physics Reference 2 | =========================== 3 | 4 | .. caution:: 5 | This documentation is work in progress! 6 | 7 | This is an unofficial documentation for the physics governing the Half-Life_ universe. There have been many very comprehensive wikis for games in the Half-Life series, such as the `Half-Life Wikia`_ and the `Combine OverWiki`_. These wikis focus on the storyline and casual gaming aspects of the Half-Life series video games. There is also a wiki for practical speedrunning aspects of these games, namely the `SourceRuns Wiki`_. Even years after Half-Life's release, one can still find casual gaming or speedrunning communities around the game. 8 | 9 | .. _Half-Life: https://en.wikipedia.org/wiki/Half-Life_(video_game) 10 | .. _Half-Life Wikia: http://half-life.wikia.com/wiki/Main_Page 11 | .. _Combine OverWiki: http://combineoverwiki.net/wiki/Main_Page 12 | .. _SourceRuns Wiki: http://wiki.sourceruns.org/wiki/Main_Page 13 | 14 | Despite the wealth of strategy guides for Half-Life, it is next to impossible to find documentations describing the physics of the game with a satisfying level of technical accuracy. One can only speculate about the reasons behind such scarcity. Knowledge about the physics of Half-Life is important for developing tools for Half-Life TAS production and for the process of TASing itself. The nature of the in-game physics demands highly precise tools. Perhaps more importantly, developing an understanding and intuition for Half-Life physics is vital in producing a highly optimised TAS for the game. 15 | 16 | Thus, this documentation strives to detail all aspects of the physics in a way that would help any curious minds to gain a much deeper appreciation for Half-Life and its speedruns. The potential tool developers will also find this documentation a helpful guide. This documentation should serve as a definitive reference material for Half-Life physics. 17 | 18 | Contents 19 | -------- 20 | 21 | .. toctree:: 22 | :numbered: 23 | :maxdepth: 2 24 | 25 | fundamentals 26 | basicphy 27 | collisions 28 | ladderphy 29 | strafing 30 | algorithms 31 | health 32 | explosions 33 | tastools 34 | glossary 35 | -------------------------------------------------------------------------------- /doc/ladderphy.rst: -------------------------------------------------------------------------------- 1 | .. _ladder-phy: 2 | 3 | Ladder physics 4 | -------------- 5 | 6 | It is widely known that the ladder climbing speed is optimisable. We first 7 | introduce :math:`\mathcal{F}` and :math:`\mathcal{S}`, which are analogues of 8 | :math:`F` and :math:`S` from the standard movement physics. Issuing 9 | ``+forward`` adds 200 to :math:`\mathcal{F}`, and issuing ``+back`` subtracts 10 | 200 from it. Thus, when both ``+forward`` and ``+back`` are issued we have 11 | :math:`\mathcal{F} = 0`. Similarly, executing ``+moveright`` adds 200 to 12 | :math:`\mathcal{S}` and ``+moveleft`` subtracts 200 from it. Note that the 13 | value of 200 cannot be modified without recompilation. For ladder physics, it 14 | does not matter what :math:`F` and :math:`S` are. If the duckstate is 2, then 15 | :math:`\mathcal{F} \mapsto 0.333\mathcal{F}` and :math:`\mathcal{S} \mapsto 16 | 0.333\mathcal{S}` in newer Half-Life versions. This is not true for earlier 17 | versions such as NGHL. Regardless of viewangles, jumping off the ladder always 18 | sets :math:`\mathbf{v}' = 270\mathbf{\hat{n}}`. 19 | 20 | If :math:`\mathbf{u} = \mathcal{F} \mathbf{\hat{f}} + \mathcal{S} 21 | \mathbf{\hat{s}}` and :math:`\mathbf{\hat{n}} \ne \langle 0,0,\pm 1\rangle` 22 | then 23 | 24 | .. math:: \mathbf{v}' = \mathbf{u} - (\mathbf{u} \cdot \mathbf{\hat{n}}) \left( \mathbf{\hat{n}} + \mathbf{\hat{n}} \times 25 | \frac{\langle 0,0,1\rangle \times \mathbf{\hat{n}}}{\lVert \langle 0,0,1\rangle \times \mathbf{\hat{n}}\rVert} \right) 26 | 27 | where :math:`\mathbf{\hat{n}}` is the unit normal vector of the ladder's 28 | climbable plane. 29 | 30 | Optimal angle between :math:`\mathbf{u}` and :math:`\mathbf{\hat{n}}` 31 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 32 | 33 | To optimise the vertical climbing speed, we assume :math:`\mathbf{\hat{n}} = 34 | \langle n_x, 0, n_z\rangle`. We further assume that :math:`u_y = 0`. Now we 35 | have 36 | 37 | .. math:: \mathbf{v}' = \mathbf{u} - \lVert\mathbf{u}\rVert \cos\alpha ( \langle n_x,0,n_z \rangle + \langle -n_z,0,n_x\rangle ) 38 | 39 | where :math:`\alpha` is the angle between :math:`\mathbf{u}` and 40 | :math:`\mathbf{\hat{n}}`. :math:`\langle -n_z,0,n_x\rangle` is actually 41 | :math:`\mathbf{\hat{n}}` rotated by :math:`\pi/2` anticlockwise when viewing 42 | into the positive direction of :math:`y`-axis. Expanding 43 | :math:`\lVert\mathbf{v}'\rVert = \sqrt{\mathbf{v}' \cdot \mathbf{v}'}`, 44 | 45 | .. math:: \begin{align*} 46 | \lVert\mathbf{v}'\rVert &= \lVert\mathbf{u}\rVert \sqrt{1 - 2\sqrt{2} \cos\alpha \cos(\alpha - \pi/4) + 2 \cos^2\alpha} \\ 47 | &= \sqrt{\mathcal{F}^2 + \mathcal{S}^2} \sqrt{1 - 2\sqrt{2} \cos^2\alpha \cos(\pi/4) - 48 | 2\sqrt{2} \cos\alpha \sin\alpha \sin(\pi/4) + 2\cos^2\alpha} \\ 49 | &= \sqrt{\mathcal{F}^2 + \mathcal{S}^2} \sqrt{1 - \sin(2\alpha)} 50 | \end{align*} 51 | 52 | We conclude that :math:`\alpha = 3\pi/4` maximises 53 | :math:`\lVert\mathbf{v}'\rVert`. If :math:`\lvert\mathcal{F}\rvert = 54 | \lvert\mathcal{S}\rvert = 200`, we have :math:`\lVert\mathbf{v}'\rVert = 400`. 55 | 56 | Knowing the optimal angle :math:`\alpha` is useful for theoretical 57 | understanding, but in practice we must be able to calculate the player's yaw 58 | and pitch angles that maximises vertical climbing speed. For ladders that are 59 | perfectly vertical the optimal viewangles are trivial to find, but we need 60 | explicit formulae for slanted ladders. 61 | 62 | Formulae for optimal yaw and pitch 63 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 64 | 65 | Let :math:`\mathbf{\hat{n}} = \langle n_x, n_y, n_z\rangle` with the constraint 66 | :math:`n_x^2 + n_y^2 + n_z^2 = 1`, so that 67 | 68 | .. math:: \mathbf{\hat{n}} + \mathbf{\hat{n}} \times 69 | \frac{\langle 0,0,1\rangle \times \mathbf{\hat{n}}} 70 | {\lVert\langle 0,0,1\rangle \times \mathbf{\hat{n}}\rVert} = 71 | \langle N_x, N_y, N_z\rangle 72 | 73 | We are concerned with the vertical velocity, :math:`v'_z`. Written in full and 74 | simplifying, 75 | 76 | .. math:: v'_z = -\mathcal{F} \sin\varphi (1 - N_z n_z) - N_z \left( 77 | \mathcal{F} \cos\varphi \cos(\vartheta - \theta) + \mathcal{S} 78 | \sin(\vartheta - \theta) \right) \sqrt{n_x^2 + n_y^2} 79 | :label: laddervz 80 | 81 | where :math:`\theta = \operatorname{atan2}(n_y,n_x)`. To maximise this 82 | quantity, we compute 83 | 84 | .. math:: \frac{\partial v'_z}{\partial\varphi} = -\mathcal{F} \cos\varphi (1 - 85 | N_z n_z) + \mathcal{F} N_z \sin\varphi \cos(\vartheta - \theta) 86 | \sqrt{n_x^2 + n_y^2} 87 | 88 | .. math:: \frac{\partial v'_z}{\partial\vartheta} = -N_z ( 89 | -\mathcal{F} \cos\varphi \sin(\vartheta - \theta) + 90 | \mathcal{S} \cos(\vartheta - \theta) ) \sqrt{n_x^2 + n_y^2} 91 | 92 | Setting them to zero and simplifying, we obtain the following equations 93 | respectively 94 | 95 | .. math:: (1 - N_z n_z) \cos\varphi = 96 | N_z \sin\varphi \cos(\vartheta - \theta) 97 | \sqrt{n_x^2 + n_y^2} 98 | :label: ladder-eq1 99 | 100 | .. math:: \mathcal{F} \cos\varphi \sin(\vartheta - \theta) = 101 | \mathcal{S} \cos(\vartheta - \theta) 102 | :label: ladder-eq2 103 | 104 | To solve these equations, we begin by assuming :math:`\lvert\mathcal{F}\rvert = 105 | \lvert\mathcal{S}\rvert \ne 0` and rewriting equation :eq:`ladder-eq2` as 106 | 107 | .. math:: \tan\varphi = \pm\frac{\sqrt{1 - 2\cos^2 (\vartheta - \theta)}} 108 | {\cos(\vartheta - \theta)} 109 | 110 | Eliminating :math:`\varphi` from equation :eq:`ladder-eq1`, we get 111 | 112 | .. math:: \frac{1 - N_z n_z}{N_z \sqrt{n_x^2 + n_y^2}} = 113 | \pm \sqrt{1 - 2\cos^2 (\vartheta - \theta)} 114 | 115 | Squaring both sides and simplifying gives 116 | 117 | .. math:: \tan^2 (\vartheta - \theta) = \frac{1}{2 n_z \sqrt{n_x^2 + n_y^2}} 118 | :label: tansqthetas 119 | 120 | Immediately we observe that :math:`n_z \ge 0` is required for this equation to have real solutions. We will deal with this in a later section. At this point we are required to take square roots. This is a critical step and we must carefully choose the signs for the numerator and the denominator, as they will determine the quadrant in which :math:`(\vartheta - \theta)` resides. 121 | 122 | We define three *free variables*: 123 | 124 | - The sign of :math:`\mathcal{S}`. Positive if rightward and negative if leftward. 125 | - The sign of :math:`\mathcal{F}`. Positive if forward and negative if backward. 126 | - The sign of :math:`v'_z`. Positive if upward and negative if downward. 127 | 128 | The motivation is that we want to be able to automatically determine the correct signs for the numerator and the denominator given our choices of the signs of the free variables. This is useful in practice because we often make conscious decisions regarding the directions in which we want to strafe when climbing ladders. For example, we may choose to invoke ``+forward`` and ``+moveleft``, or ``+back`` and ``+moveright``. In both cases the resulting velocity is identically optimal, and yet the viewangles are different. By declaring the signs of :math:`\mathcal{S}` and :math:`\mathcal{F}` as free variables, we can choose the strafing directions mathematically by simply setting the correct signs. 129 | 130 | Optimal ladder climbing can go in two possible directions, that is upward or downward. Again, the maximum climbing speed does not depend on the direction, though the viewangles do. Hence we declare the sign of :math:`v'_z` as a free variable. 131 | 132 | We will now attempt to formulate the final viewangles in terms of these free variables. To begin, we examine Equation :eq:`laddervz` more closely. We make three observations: 133 | 134 | #. We have :math:`1 - N_z n_z \ge 0` when :math:`0 \le n_z \le 1/\sqrt{2}` and :math:`1 - N_z n_z < 0` when :math:`1/\sqrt{2} < n_z \le 1`. 135 | 136 | #. We have :math:`N_z > 0`. 137 | 138 | #. We have :math:`\cos\varphi \ge 0` for :math:`-\pi/2 \le \varphi \le \pi/2`. 139 | 140 | We start by considering the sign of :math:`v'_z`. Obviously, the right hand side of Equation :eq:`laddervz` must have the same sign as the :math:`v'_z`. But observe that there are two terms in the right hand side. Therefore, both terms should also be as large as possible in the direction indicated by the sign of :math:`v'_z`. For example, if we choose :math:`v'_z < 0`, then the terms on the right hand side should be as negative as possible, and vice versa. 141 | 142 | We will deal with the angle :math:`(\vartheta - \theta)` first, which appears only in the second term, so we will assume that the first term has been dealt with (that is, conforming to the sign of :math:`v'_z` while being as large as possible in magnitude). Now, we want 143 | 144 | .. math:: \operatorname{sgn}(v'_z) = \operatorname{sgn}\left( -N_z (\mathcal{F} \cos\varphi \cos(\vartheta - \theta) + \mathcal{S} \sin(\vartheta - \theta)) \sqrt{n_x^2 + n_y^2} \right) 145 | 146 | By one of the observations we made, we have :math:`N_z > 0` and :math:`\cos\varphi \ge 0`. Also, :math:`\sqrt{n_x^2 + n_y^2}` is always positive. Hence, equivalently we need 147 | 148 | .. math:: \operatorname{sgn}(v'_z) = -\operatorname{sgn}( \mathcal{F} \cos(\vartheta - \theta) + \mathcal{S} \sin(\vartheta - \theta) ) 149 | 150 | And further, 151 | 152 | .. math:: 153 | \begin{align*} 154 | \operatorname{sgn}(v'_z) &= -\operatorname{sgn}(\mathcal{F} \cos(\vartheta - \theta)) \\ 155 | \operatorname{sgn}(v'_z) &= -\operatorname{sgn}(\mathcal{S} \sin(\vartheta - \theta)) 156 | \end{align*} 157 | 158 | And thus, 159 | 160 | .. math:: 161 | \begin{align*} 162 | \operatorname{sgn}(\sin(\vartheta - \theta)) &= -\operatorname{sgn}(\mathcal{F} v'_z) \\ 163 | \operatorname{sgn}(\cos(\vartheta - \theta)) &= -\operatorname{sgn}(\mathcal{S} v'_z) 164 | \end{align*} 165 | 166 | Observe that the required signs of :math:`\sin(\vartheta - \theta)` and :math:`\cos(\vartheta - \theta)` depends on the chosen signs of :math:`\mathcal{F}` and :math:`\mathcal{S}` respectively, in addition to the sign of :math:`v'_z`. If we look at Equation :eq:`tansqthetas` again, notice that the signs of :math:`\sin(\vartheta - \theta)` and :math:`\cos(\vartheta - \theta)` determine the signs of the numerator and denominator respectively after removing the squares, because :math:`\tan(x) = \sin(x) / \cos(x)` for all :math:`x`. 167 | 168 | Deriving from Equation :eq:`tansqthetas`, the formula for the optimal yaw is thus, in all its glory, 169 | 170 | .. math:: \vartheta = \operatorname{atan2}(n_y, n_x) + 171 | \operatorname{atan2}\left( -\operatorname{sgn}(\mathcal{S} v'_z),\; 172 | -\operatorname{sgn}(\mathcal{F} v'_z) \sqrt{2 n_z \sqrt{n_x^2 + 173 | n_y^2}} \right) 174 | :label: ladder-vartheta 175 | 176 | We can adopt the same line of attack for the final formula for :math:`\varphi`. Combining Equation :eq:`ladder-eq2` and Equation :eq:`tansqthetas` gives 177 | 178 | .. math:: \cos\varphi = \cot(\vartheta - \theta) = \sqrt{2 n_z \sqrt{n_x^2 + n_y^2}} 179 | 180 | Note that the positive square root is taken for the cotangent term because we want :math:`-\pi/2 \le \varphi \le \pi/2`. This is followed by a simple rewrite: 181 | 182 | .. math:: \varphi = \pm \arccos \sqrt{2 n_z \sqrt{n_x^2 + n_y^2}} 183 | 184 | Here, we only need to determine the sign of the right hand side as a whole, rather than considering the numerator and the denominator separately. The sign of :math:`\varphi` will indicate whether the player should look upward or downward when climbing. Going back to Equation :eq:`laddervz` again, we assume the second term has been dealt with, in the same way we assumed the first term to have been dealt with when deducing the signs for the optimal yaw. Now we must have 185 | 186 | .. math:: \operatorname{sgn}(v'_z) = \operatorname{sgn}(-\mathcal{F} \sin\varphi (1 - N_z n_z)) 187 | 188 | Since the sign of :math:`\sin\varphi` is completely determined by the sign of :math:`\varphi`, the relation is simplified to 189 | 190 | .. math:: \operatorname{sgn}(v'_z) = -\operatorname{sgn}(\mathcal{F} \varphi (1 - N_z n_z)) 191 | 192 | And equivalently, 193 | 194 | .. math:: \operatorname{sgn}(\varphi) = -\operatorname{sgn}(\mathcal{F} v'_z (1 - N_z n_z)) 195 | 196 | Notice that the sign of :math:`(1 - N_z n_z)` plays a role here. In practice, however, :math:`1 - N_z n_z` is less efficient to compute. Using one of the observations, we see that :math:`\operatorname{sgn}(1 - N_z n_z) = \operatorname{sgn}\left( 1/\sqrt{2} - n_z \right)`. So we are done and we can write out the complete formula for the optimal pitch as follows: 197 | 198 | .. math:: \varphi = -\operatorname{sgn}\left( \mathcal{F} v'_z \left(1/\sqrt{2} - n_z\right) \right) 199 | \arccos\sqrt{2 n_z \sqrt{n_x^2 + n_y^2}} 200 | :label: ladder-varphi 201 | 202 | Optimal yaw and pitch when :math:`n_z < 0` 203 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 204 | 205 | When :math:`n_z < 0`, the derivatives will never be zero. However, we can 206 | observe that :math:`\lvert\varphi\rvert` increases when :math:`n_z` decreases. 207 | We also note we constrain the range of :math:`\varphi` to :math:`[-\pi/2, 208 | \pi/2]` while the value of :math:`\vartheta` is unrestricted. Hence we can 209 | substitute the maximum value :math:`\lvert\varphi\rvert = \pi/2` into 210 | :math:`\partial v'_z/\partial\varphi = 0` and solve for :math:`\vartheta`. It 211 | is found to be 212 | 213 | .. math:: \vartheta = \theta \pm \frac{\pi}{2} 214 | 215 | We need to determine what the sign of :math:`\pi/2` means. Substituting 216 | :math:`\varphi = \pm\pi/2` and :math:`\vartheta - \theta = \pm\pi/2` into the 217 | original vertical velocity equation gives 218 | 219 | .. math:: v'_z = -\mathcal{F} \operatorname{sgn}(\varphi) (1 - N_z n_z) - N_z 220 | \mathcal{S} \operatorname{sgn}(\vartheta - \theta) \sqrt{n_x^2 + 221 | n_y^2} 222 | 223 | Note that :math:`N_z < 0` when :math:`n_z < -1/\sqrt{2}`. Now we can use the 224 | similar technique to deduce the required signs of :math:`\varphi` and 225 | :math:`(\vartheta - \theta)`, which results in 226 | 227 | .. math:: \vartheta = \operatorname{atan2}(n_y,n_x) + 228 | \operatorname{sgn}(\mathcal{S} v_z' (n_z + 1/\sqrt{2})) \frac{\pi}{2} 229 | 230 | .. math:: \varphi = -\operatorname{sgn}(\mathcal{F} v'_z) \frac{\pi}{2} 231 | 232 | Again, we wrote these formulae so that they give the correct angles given the 233 | freely chosen signs of :math:`\mathcal{S}`, :math:`\mathcal{F}` and 234 | :math:`v'_z`. 235 | 236 | Optimal yaw and pitch when :math:`n_z = 1` 237 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 238 | 239 | Up to this point we have been assuming the normal vector not being vertical. 240 | If :math:`\mathbf{\hat{n}} = \langle 0,0,\pm 1\rangle`, then the second term in 241 | the bracket vanishes (since ``VectorNormalize`` in ``pm_shared/pm_math.c`` 242 | returns a zero vector if the input, which is :math:`\langle 0,0,1\rangle \times 243 | \mathbf{\hat{n}}`, is also a zero vector) instead of being indeterminate, 244 | leaving only 245 | 246 | .. math:: \mathbf{v}' = \mathbf{u} - \lVert\mathbf{u}\rVert \cos\alpha \langle 0,0,\pm 1\rangle 247 | 248 | thus 249 | 250 | .. math:: \lVert\mathbf{v}'\rVert = \sqrt{\mathcal{F}^2 + \mathcal{S}^2} \sqrt{1 - \cos^2 \alpha} 251 | 252 | which is maximised when :math:`\alpha = \pi/2`. This can be achieved by 253 | setting :math:`\varphi = 0`. If :math:`\lvert\mathcal{F}\rvert = 254 | \lvert\mathcal{S}\rvert \ne 0` then the yaw should be 45 or 135 degrees away 255 | from the intended direction, depending on the signs. 256 | -------------------------------------------------------------------------------- /doc/strafing.rst: -------------------------------------------------------------------------------- 1 | Strafing physics 2 | ================ 3 | 4 | Here we will be discussing movement physics primarily associated with the 5 | player. Physics of other entities such as monster and boxes are presumed to 6 | share some similarities with the player, though they are usually not important. 7 | 8 | 9 | Fundamental movement equation 10 | ----------------------------- 11 | 12 | For subsequence analyses that involve mathematics, we will not concern 13 | ourselves with the details of how they were implemented in ``PM_AirMove`` and 14 | ``PM_WalkMove``. For example, we will not be thinking in terms of ``wishvel``, 15 | ``addspeed``, ``pmove->right`` and so on. 16 | 17 | Denote :math:`\mathbf{v}` the player velocity in the current frame and 18 | :math:`\mathbf{v}'` the new velocity. Denote :math:`\mathbf{\hat{a}}` the unit 19 | acceleration vector. Let :math:`\theta` the angle between :math:`\mathbf{v}` 20 | and :math:`\mathbf{\hat{a}}`. Then 21 | 22 | .. math:: \mathbf{v}' = \mathbf{v} + \mu\mathbf{\hat{a}} 23 | :label: newvel 24 | 25 | with 26 | 27 | .. math:: \mu = 28 | \begin{cases} 29 | \min(\gamma_1, \gamma_2) & \text{if } \gamma_2 > 0 \\ 30 | 0 & \text{otherwise} 31 | \end{cases} 32 | \quad\quad 33 | M = \min\left( M_m, \sqrt{F^2 + S^2} \right) \\ 34 | \gamma_1 = k_e \tau MA 35 | \quad\quad 36 | \gamma_2 = L - \mathbf{v} \cdot \mathbf{\hat{a}} = L - \lVert\mathbf{v}\rVert \cos\theta 37 | :nowrap: 38 | 39 | where :math:`\tau` is called the *frame time*, which is just the inverse of 40 | frame rate. :math:`L = 30` when airstrafing and :math:`L = M` when 41 | groundstrafing. :math:`A` is the value of ``sv_airaccelerate`` when 42 | airstrafing and ``sv_accelerate`` when groundstrafing. Lastly, :math:`k_e` is 43 | called the *environmental friction* which is usually 1 and will be explained in 44 | :ref:`friction`. 45 | 46 | Ignoring the roll angle, the unit acceleration vector is such that 47 | :math:`\mathbf{a} = F \mathbf{\hat{f}} + S \mathbf{\hat{s}}`, which is in 48 | :math:`\mathbb{R}^2`. We have unit forward vector :math:`\mathbf{\hat{f}} = 49 | \langle\cos\vartheta, \sin\vartheta\rangle` where :math:`\vartheta` is the yaw 50 | angle. This means :math:`\mathbf{\hat{f}}` is essentially directed parallel to 51 | the player view. :math:`\mathbf{\hat{s}}` is directed perpendicular to 52 | :math:`\mathbf{\hat{f}}` rightward, or :math:`\mathbf{\hat{s}} = 53 | \langle\sin\vartheta, -\cos\vartheta\rangle`. The magnitude of 54 | :math:`\mathbf{a}` is simply :math:`\sqrt{F^2 + S^2}`. In the following 55 | analysis we will not concern ourselves with the components of 56 | :math:`\mathbf{a}`, but instead parameterise the entire equation in 57 | :math:`\theta`. 58 | 59 | Besides, we will assume that :math:`\lVert\langle F,S\rangle\rVert \ge M`. If 60 | this is not the case, we must replace :math:`M \mapsto \lVert\langle 61 | F,S\rangle\rVert` for all appearances of :math:`M` below. Throughout this 62 | document we will assume that :math:`M`, :math:`A`, :math:`\tau`, :math:`k`, 63 | :math:`k_e` and :math:`E` are positive. 64 | 65 | 66 | Optimal strafing 67 | ---------------- 68 | 69 | The new speed :math:`\lVert\mathbf{v}'\rVert` can be expressed as 70 | 71 | .. math:: \lVert\mathbf{v}'\rVert = \sqrt{\mathbf{v}' \cdot \mathbf{v}'} = 72 | \sqrt{(\mathbf{v} + \mu\mathbf{\hat{a}}) \cdot (\mathbf{v} + \mu\mathbf{\hat{a}})} = 73 | \sqrt{\lVert\mathbf{v}\rVert^2 + \mu^2 + 2\lVert\mathbf{v}\rVert \mu \cos\theta} 74 | :label: nextspeed 75 | 76 | Equation :eq:`nextspeed`, sometimes called the *scalar FME*, is often used in 77 | practical applications as the general way to compute new speeds, if 78 | :math:`\theta` is known. To compute new velocity vectors, given 79 | :math:`\theta`, we can rewrite Equation :eq:`newvel` as 80 | 81 | .. math:: \mathbf{v}' = \mathbf{v} + \mu\mathbf{\hat{v}} 82 | \begin{pmatrix} 83 | \cos\theta & \mp\sin\theta \\ 84 | \pm\sin\theta & \cos\theta 85 | \end{pmatrix} \quad\quad \text{if } \mathbf{v} \ne \mathbf{0} 86 | :label: newvelmat 87 | 88 | which expresses :math:`\mathbf{\hat{a}}` as a rotation of 89 | :math:`\mathbf{\hat{v}}` clockwise or anticlockwise, depending on the signs of 90 | :math:`\sin\theta`. Equation :eq:`newvelmat` can be useful when computing line 91 | strafing. 92 | 93 | If :math:`\mu = \gamma_1` and :math:`\mu = \gamma_2` we have 94 | 95 | .. math:: \begin{align*} 96 | \lVert\mathbf{v}'\rVert_{\mu = \gamma_1} &= \sqrt{\lVert\mathbf{v}\rVert^2 + 97 | k_e \tau MA \left( k_e \tau MA + 2 \lVert\mathbf{v}\rVert \cos\theta \right)} \\ 98 | \lVert\mathbf{v}'\rVert_{\mu = \gamma_2} &= \sqrt{\lVert\mathbf{v}\rVert^2 \sin^2 \theta + L^2} 99 | \end{align*} 100 | 101 | respectively. Let :math:`\theta` the independent variable, then notice that 102 | these functions are invariant under the transformation :math:`\theta \mapsto 103 | -\theta`. Hence we will consider only :math:`\theta \ge 0` for simplicity. 104 | Observe that 105 | 106 | 1. :math:`\lVert\mathbf{v}'\rVert_{\mu = \gamma_1}` and 107 | :math:`\lVert\mathbf{v}'\rVert_{\mu = \gamma_2}` intersects only at 108 | :math:`\theta = \zeta` where :math:`\cos\zeta = (L - k_e \tau MA) 109 | \lVert\mathbf{v}\rVert^{-1}` is obtained by solving :math:`\gamma_1 = 110 | \gamma_2` 111 | 112 | 2. :math:`\lVert\mathbf{v}'\rVert_{\mu = \gamma_1}` is decreasing in :math:`0 113 | \le \theta \le \pi` 114 | 115 | 3. :math:`\lVert\mathbf{v}'\rVert_{\mu = \gamma_2}` is increasing in :math:`0 116 | \le \theta \le \pi/2` and decreasing in :math:`\pi/2 \le \theta \le \pi` 117 | 118 | 4. :math:`\mu = \gamma_2` if :math:`0 \le \theta \le \zeta`, and :math:`\mu = 119 | \gamma_1` if :math:`\zeta < \theta \le \pi`. 120 | 121 | Therefore, we claim that to maximise :math:`\lVert\mathbf{v}'\rVert` we have 122 | optimal angle 123 | 124 | .. math:: \theta = 125 | \begin{cases} 126 | \pi/2 & \text{if } L - k_e \tau MA \le 0 \\ 127 | \zeta & \text{if } 0 < L - k_e \tau MA \le \lVert\mathbf{v}\rVert \\ 128 | 0 & \text{otherwise} 129 | \end{cases} 130 | 131 | To see this, suppose :math:`0 < \theta < \pi/2`. This implies the second 132 | condition described above. When this is the case, the always decreasing curve 133 | of :math:`\lVert\mathbf{v}'\rVert_{\mu=\gamma_1}` intersects that of 134 | :math:`\lVert\mathbf{v}'\rVert_{\mu=\gamma_2}` at the point where the latter 135 | curve is increasing. To the left of this point is the domain of the latter 136 | curve, which is increasing until we reach the discontinuity at the point of 137 | intersection, beyond which is the domain of the former curve. Therefore the 138 | optimal angle is simply at the peak: the point of intersection :math:`\theta = 139 | \zeta`. 140 | 141 | If :math:`\theta \ge \pi/2`, the former curve intersects the latter curve at 142 | the point where the latter is decreasing. :math:`0 \le \theta \le \zeta` is 143 | the domain of the latter curve which contains the maximum point at 144 | :math:`\pi/2`. Have a look at the graphs below: 145 | 146 | .. image:: _static/optang-1.png 147 | 148 | Note that these are sketches of the real graphs, therefore they are by no means 149 | accurate. However, they do illustrate the four observations made above 150 | accurately. The green dashed lines represent the curve of 151 | :math:`\lVert\mathbf{v}'\rVert_{\mu=\gamma_1}`, which is always decreasing 152 | (observation 2). The blue dashed lines represent 153 | :math:`\lVert\mathbf{v}'\rVert_{\mu=\gamma_2}`, which fits observation 3. Now 154 | focus on the red lines: they represent the graph of 155 | :math:`\lVert\mathbf{v}'\rVert` if the restriction :math:`\mu = \min(\gamma_1, 156 | \gamma_2)` is factored in, rather than considering each case in isolation. In 157 | other words, the red lines are what we expect to obtain if we sketch them using 158 | Equation :eq:`nextspeed`. Notice that the region :math:`0 \le \theta \le 159 | \zeta` is indeed the domain of :math:`\lVert\mathbf{v}'\rVert_{\mu=\gamma_2}`, 160 | and vice versa (observation 4). Finally, the blue line and green line 161 | intersect only at one point. Now it is clear where the maximum points are, 162 | along with the optimal :math:`\theta`\ s associated with them. 163 | 164 | Having these results, for airstrafing it is a matter of simple substitutions to 165 | obtain 166 | 167 | .. math:: \lVert\mathbf{v}_n\rVert = 168 | \begin{cases} 169 | \sqrt{\lVert\mathbf{v}\rVert^2 + 900n} & \text{if } \theta = \pi/2 \\ 170 | \sqrt{\lVert\mathbf{v}\rVert^2 + nk_e \tau MA_a (60 - k_e \tau MA_a)} & \text{if } \theta = \zeta \\ 171 | \lVert\mathbf{v}\rVert + nk_e \tau MA_a & \text{if } \theta = 0 172 | \end{cases} 173 | 174 | These equations can be quite useful in planning. For example, to calculate the 175 | number of frames required to airstrafe from :math:`320` ups to :math:`1000` ups 176 | at default Half-Life settings, we solve 177 | 178 | .. math:: 1000^2 = 320^2 + n \cdot 0.001 \cdot 320 \cdot 10 \cdot (60 - 0.001 \cdot 320 \cdot 10) \\ 179 | \implies n \approx 4938 180 | :nowrap: 181 | 182 | For groundstrafing, however, the presence of friction means simple substitution 183 | may not work. 184 | 185 | 186 | .. _friction: 187 | 188 | Friction 189 | -------- 190 | 191 | Let :math:`k` the friction coefficient, :math:`k_e` the environmental friction 192 | and :math:`E` the stopspeed. The value of :math:`k` in the game 193 | ``sv_friction`` while :math:`E` is ``sv_stopspeed``. As mentioned previously, 194 | in most cases :math:`k_e = 1` unless the player is standing on a friction 195 | modifier. If friction is present, then before any physics computation is done, 196 | the velocity must be multiplied by :math:`\lambda` such that 197 | 198 | .. math:: \lambda = \max(1 - \max(1, E \lVert\mathbf{v}\rVert^{-1}) k_e k\tau, 0) 199 | :label: fricfunc 200 | 201 | In :math:`Ek\tau \le \lVert\mathbf{v}\rVert \le E`, the kind of friction is 202 | called *arithmetic friction*. It is so named because if the player is allowed 203 | to slide freely on the ground, the successive speeds form an arithmetic series. 204 | In other words, given initial speed, the speed at the :math:`n`\ -th frame 205 | :math:`\lVert\mathbf{v}_n\rVert` is 206 | 207 | .. math:: \lVert\mathbf{v}_n\rVert = \lVert\mathbf{v}_0\rVert - nEk_ek\tau 208 | 209 | Let :math:`t = n\tau`, then notice that the value of 210 | :math:`\lVert\mathbf{v}_t\rVert` is independent of the frame rate. If 211 | :math:`\lVert\mathbf{v}\rVert > E`, however, the friction is called *geometric 212 | friction* 213 | 214 | .. math:: \lVert\mathbf{v}_n\rVert = \lVert\mathbf{v}_0\rVert (1 - k_ek\tau)^n 215 | 216 | Again, let :math:`t = n\tau`, then :math:`\lVert\mathbf{v}_t\rVert = 217 | \lVert\mathbf{v}_0\rVert (1 - k\tau)^{t/\tau}`. Observe that 218 | 219 | .. math:: \frac{d}{d\tau} \lVert\mathbf{v}_t\rVert = -\frac{t}{\tau} 220 | \lVert\mathbf{v}_t\rVert \left( \frac{k_ek}{1 - k_ek\tau} + 221 | \frac{\ln\lvert 1 - k_ek\tau\rvert}{\tau} \right) \le 0 \quad\text{for } t \ge 0 222 | 223 | which means :math:`\lVert\mathbf{v}_t\rVert` is strictly increasing with 224 | respect to :math:`\tau` at any given positive :math:`t`. By increasing 225 | :math:`\tau` (or decreasing the frame rate), the deceleration as a result of 226 | geometric friction becomes larger. 227 | 228 | There is a limit to the speed achievable by perfect groundstrafing alone. 229 | There will be a critical speed such that the increase in speed exactly cancels 230 | the friction, so that :math:`\lVert\mathbf{v}_{n + 1}\rVert = 231 | \lVert\mathbf{v}_n\rVert`. For example, suppose optimal :math:`\theta = \zeta` 232 | and geometric friction is at play. Then if 233 | 234 | .. math:: \lVert\mathbf{v}\rVert^2 = (1 - k_e k\tau)^2 \lVert\mathbf{v}\rVert^2 + k_e \tau M^2 A_g (2 - k_e \tau A_g) 235 | 236 | we have *maximum groundstrafe speed* 237 | 238 | .. math:: M \sqrt{\frac{A_g (2 - k_e \tau A_g)}{k (2 - k_ek\tau)}} 239 | 240 | Strafing at this speed effectively degenerates *perfect strafing* into *speed 241 | preserving strafing*, which will be discussed shortly after. If :math:`k < 242 | A_g`, which is the case in default Half-Life settings, the smaller the 243 | :math:`\tau` the higher the maximum groundstrafe speed. If :math:`\theta = 244 | \pi/2` instead, then the expression becomes 245 | 246 | .. math:: \frac{M}{\sqrt{k_ek\tau (2 - k_ek \tau)}} 247 | 248 | 249 | Bunnyhop cap 250 | ------------ 251 | 252 | We must introduce :math:`M_m`, which is the value of ``sv_maxspeed``. It is 253 | not always the case that :math:`M_m = M`, since :math:`M` can be affected by 254 | duckstate and the values of :math:`F`, :math:`S` and :math:`U`. 255 | 256 | All Steam versions of Half-Life have an infamous "cap" on bunnyhop speed which 257 | is triggered only when jumping with player speed greater than :math:`1.7M_m`. 258 | Note that the aforementioned speed is not horizontal speed, but rather, the 259 | magnitude of the entire :math:`\mathbb{R}^3` vector. When this mechanism is 260 | triggered, the new velocity will become :math:`1.105 M_m \mathbf{\hat{v}}`. 261 | 262 | It is impossible to avoid this mechanism when jumping. In speedruns a 263 | workaround would be to ducktap instead, but each ducktap requires the player to 264 | slide on the ground for one frame, thereby losing a bit of speed due to 265 | friction. In addition, a player cannot ducktap if there is insufficient space 266 | above him. In this case jumping is the only way to maintain speed, though 267 | there are different possible styles to achieve this. 268 | 269 | One way would be to move at constant horizontal speed, which is :math:`1.7M_m`. 270 | The second way would be to accelerate while in the air, then backpedal after 271 | landing on the ground until the speed reduces to :math:`1.7M_m` before jumping 272 | off again. Yet another way would be to accelerate in the air *and* on the 273 | ground, though the speed will still decrease while on the ground as long as the 274 | speed is greater than the maximum groundstrafe speed. To the determine the 275 | most optimal method we must compare the distance travelled for a given number 276 | of frames. We will assume that the maximum groundstrafe speed is lower than 277 | :math:`1.7M_m`. 278 | 279 | It turns out that the answer is not as straightforward as we may have thought. 280 | 281 | TODO!! 282 | 283 | 284 | Air-ground speed threshold 285 | -------------------------- 286 | 287 | The acceleration of groundstrafe is usually greater than that of airstrafe. It 288 | is for this reason that groundstrafing is used to initiate bunnyhopping. 289 | However, once the speed increases beyond :math:`E` the acceleration will begin 290 | to decrease, as the friction grows proportionally with the speed. There will 291 | be a critical speed beyond which the acceleration of airstrafe exceeds that of 292 | groundstrafe. This is called the *air-ground speed threshold* (AGST), 293 | admittedly a rather non-descriptive name. 294 | 295 | Analytic solutions for AGST are always available, but they are cumbersome to 296 | write and code. Sometimes the speed curves for airstrafe and groundstrafe 297 | intercepts several times, depending even on the initial speed itself. A more 298 | practical solution in practice is to simply use Equation :eq:`nextspeed` to 299 | compute the new airstrafe and groundstrafe speeds then comparing them. 300 | 301 | 302 | Speed preserving strafing 303 | ------------------------- 304 | 305 | Speed preserving strafing can be useful when we are strafing at high :math:`A`. 306 | It takes only about 4.4s to reach 2000 ups from rest at :math:`A = 100`. While 307 | making turns at 2000 ups, if the velocity is not parallel to the global axes 308 | the speed will exceed ``sv_maxvelocity``. Ocassionally, this can prove 309 | cumbersome as the curvature decreases with increasing speed, making the player 310 | liable to collision with walls or other obstacles. Besides, as the velocity 311 | gradually becomes parallel to one of the global axes again, the speed will drop 312 | back to ``sv_maxvelocity``. This means, under certain situations, that the 313 | slight speed increase in the process of making the turn has little benefit. 314 | Therefore, it can sometimes be helpful to simply make turns at a constant 315 | ``sv_maxvelocity``. This is where the technique of *speed preserving strafing* 316 | comes into play. Another situation might be that we want to groundstrafe at a 317 | constant speed. When the speed is relatively low, constant speed 318 | groundstrafing can produce a very sharp curve, which is sometimes desirable in 319 | a very confined space. 320 | 321 | We first consider the case where friction is absent. Setting 322 | :math:`\lVert\mathbf{v}'\rVert = \lVert\mathbf{v}\rVert` in Equation 323 | :eq:`nextspeed` and solving, 324 | 325 | .. math:: \cos\theta = -\frac{\mu}{2\lVert\mathbf{v}\rVert} 326 | 327 | If :math:`\mu = \gamma_1` then we must have :math:`\gamma_1 \le \gamma_2`, or 328 | 329 | .. math:: k_e \tau MA \le L - \lVert\mathbf{v}\rVert \cos\theta \implies k_e \tau MA \le 2L 330 | 331 | At this point we can go ahead and write out the full formula for :math:`\theta` 332 | that preserves speed while strafing 333 | 334 | .. math:: \cos\theta = 335 | \begin{cases} 336 | -\displaystyle\frac{k_e \tau MA}{2\lVert\mathbf{v}\rVert} & \text{if } k_e \tau MA \le 2L \\ 337 | -\displaystyle\frac{L}{\lVert\mathbf{v}\rVert} & \text{otherwise} 338 | \end{cases} 339 | 340 | On the other hand, if friction is present, we let :math:`\lVert\mathbf{u}\rVert 341 | = \lambda\lVert\mathbf{v}\rVert` be the speed immediately after friction is 342 | applied, where :math:`\lambda` is given in :eq:`fricfunc`. Now we have 343 | 344 | .. math:: \lVert\mathbf{v}\rVert^2 = \lVert\mathbf{u}\rVert^2 + \mu^2 + 2 \mu 345 | \lVert\mathbf{u}\rVert \cos\theta 346 | 347 | By the usual line of attack, we force :math:`\mu = \gamma_1` which implies that 348 | :math:`\gamma_1 \le \gamma_2`, giving the formula 349 | 350 | .. math:: \cos\theta = \frac{1}{2\lVert\mathbf{u}\rVert} \left( 351 | \frac{\lVert\mathbf{v}\rVert^2 - \lVert\mathbf{u}\rVert^2}{k_e \tau MA} - 352 | k_e \tau MA \right) 353 | 354 | and the necessary condition 355 | 356 | .. math:: \frac{\lVert\mathbf{v}\rVert^2 - \lVert\mathbf{u}\rVert^2}{k_e \tau 357 | MA} + k_e \tau MA\le 2L 358 | 359 | If that condition failed, then we instead have 360 | 361 | .. math:: \cos\theta = -\frac{\sqrt{L^2 - \left( \lVert\mathbf{v}\rVert^2 - 362 | \lVert\mathbf{u}\rVert^2 \right)}}{\lVert\mathbf{u}\rVert} 363 | 364 | Note that we took the negative square root, because :math:`\theta` needs to be 365 | as large as possible so that the curvature of the strafing path is maximised, 366 | which is one of the purposes of speed preserving strafing. To derive the 367 | necessary condition for the formula above, we again employ the standard 368 | strategy, yielding 369 | 370 | .. math:: k_e \tau MA - L > \sqrt{L^2 - \left( \lVert\mathbf{v}\rVert^2 - 371 | \lVert\mathbf{u}\rVert^2 \right)} 372 | 373 | Observe that we need :math:`k_e \tau MA > L` and :math:`L^2 \ge 374 | \lVert\mathbf{v}\rVert^2 - \lVert\mathbf{u}\rVert^2`. Then we square the 375 | inequality to yield the converse of the condition for :math:`\mu = \gamma_1`, 376 | as expected. Putting these results together, we obtain 377 | 378 | .. math:: \cos\theta = 379 | \begin{cases} 380 | \displaystyle \frac{1}{2\lVert\mathbf{u}\rVert} \left( 381 | \frac{\lVert\mathbf{v}\rVert^2 - \lVert\mathbf{u}\rVert^2}{k_e \tau MA} - 382 | k_e \tau MA \right) & \displaystyle \text{if } \frac{\lVert\mathbf{v}\rVert^2 - 383 | \lVert\mathbf{u}\rVert^2}{k_e \tau MA} + k_e \tau MA\le 2L \\ 384 | \displaystyle -\frac{\sqrt{L^2 - \left( \lVert\mathbf{v}\rVert^2 - 385 | \lVert\mathbf{u}\rVert^2 \right)}}{\lVert\mathbf{u}\rVert} & 386 | \displaystyle \text{otherwise, if } k_e \tau MA > L \text{ and } L^2 \ge 387 | \lVert\mathbf{v}\rVert^2 - \lVert\mathbf{u}\rVert^2 388 | \end{cases} 389 | 390 | Note that, regardless of whether friction is present, if 391 | :math:`\lvert\cos\theta\rvert > 1` then we might resort to using the optimal 392 | angle to strafe instead. This can happen when, for instance, the speed is so 393 | small that the player will always gain speed regardless of strafing direction. 394 | Or it could be that the effect of friction exceeds that of strafing, rendering 395 | it impossible to prevent the speed reduction. If 396 | :math:`\lVert\mathbf{v}\rVert` is greater than the maximum groundstrafe speed, 397 | then the angle that minimises the inevitable speed loss is obviously the 398 | optimal strafing angle. 399 | 400 | 401 | Curvature 402 | --------- 403 | 404 | The locus of a point obtained by strafing is a spiral. Intuitively, at any given speed there is a limit to how sharp a turn can be made without lowering acceleration. It is commonly known that this limit grows harsher with higher speed. As tight turns are common in Half-Life, this becomes an important consideration that preoccupies speedrunners at almost every moment. Learning how navigate through tight corners by strafing without losing speed is a make-or-break skill in speedrunning. 405 | 406 | It is natural to ask exactly how this limit can be quantified for the benefit of TASing. The simplest way to do so is to consider the *radius of curvature* of the path. Obviously, this quantity is not constant with time, except for speed preserving strafing. Therefore, when we talk about the radius of curvature, precisely we are referring to the *instantaneous* radius of curvature, namely the radius at a given instant in time. But time is discrete in Half-Life, so this is approximated by the radius in a given frame. 407 | 408 | 90 degrees turns 409 | ~~~~~~~~~~~~~~~~ 410 | 411 | Passageways in Half-Life commonly bend perpendicularly, so we frequently make 90 degrees turns by strafing. We can imagine how the width of a passage limits the maximum radius of curvature one can sustain without colliding with the walls. This implies that the speed is limited as well. When planning for speedruns, it can prove useful to be able to estimate this limit for a given turn without running a simulation or strafing by hand. In particular, we want to compute the maximum speed for a given passage width. We start by making some simplifying assumptions that will greatly reduce the difficulty of analysis while closely modelling actual situations in practice. Refer to the figure below. 412 | 413 | .. image:: _static/90-degrees-strafe-radius.png 414 | :height: 800px 415 | :width: 754px 416 | :scale: 50% 417 | :align: center 418 | 419 | The first assumption we make is that the width of the corridor is the same before and after the turn. This width is denoted as :math:`d`, as one can see in the figure. This assumption is justified because this is often true or approximately true in Half-Life maps. The second assumption is that the path is circular. The centre of this circle, also named the *centre of curvature*, is at point :math:`C`. As noted earlier, the strafing path is in general a spiral with varying radius of curvature. Nevertheless, the total time required to make such a turn is typically very small. Within such short time frame, the radius would not have changed significantly. Therefore it is not absurd to assume that the radius of curvature is constant while making the turn. The third assumption is that the positions of the player before and after making the turn coincide with the walls. This assumption is arguably less realistic, but the resulting path is the larger circular arc one can fit in this space. 420 | 421 | By trivial applications of the Pythagorean theorem, it can be shown that the relationship between the radius of curvature :math:`r` and the width of the corridor :math:`d` is given by 422 | 423 | .. math:: r = \left( 2 + \sqrt{2} \right) d \approx 3.414 d 424 | 425 | This formula may be used to estimate the maximum radius of curvature for making such a turn without collision. However, the radius of curvature by itself is not very useful. We may wish to further estimate the maximum speed corresponding to this :math:`r`. 426 | 427 | Radius-speed relationship 428 | ~~~~~~~~~~~~~~~~~~~~~~~~~ 429 | 430 | The following figure depicts the positions of the player at times :math:`t = 0`, :math:`t = \tau` and :math:`t = 2\tau`. The initial speed is :math:`\lVert\mathbf{v}\rVert`. All other symbols have their usual meaning. 431 | 432 | .. image:: _static/radius-estimate-xy.png 433 | :height: 775px 434 | :width: 1135px 435 | :scale: 50% 436 | :align: center 437 | 438 | Based on the figure, the radius of curvature may be approximated as the :math:`y`-intercept, or :math:`c`. Obviously, a more accurate approximation may be achieved by averaging :math:`c` and :math:`\mathit{BC}`. However, this results in a clumsy formula with little benefit. Empirically, the approximation by calculating :math:`c` is sufficiently accurate in practice. In consideration of this, it can be calculated that 439 | 440 | .. math:: r \approx c = \frac{\tau}{\sin\theta} \left( \frac{2}{\mu} \lVert\mathbf{v}\rVert^2 + 3 \lVert\mathbf{v}\rVert \cos\theta + \mu \right) 441 | :label: radius-speed-relationship 442 | 443 | Note that this is the most general formula, applicable to any type of strafing. From this equation, observe that the radius of curvature grows with the square of speed. This is a fairly rapid growth. On the other hand, under maximum speed strafing, the speed grows with the square root of time. Informally, the result of these two growth rates conspiring with one another is that the radius of curvature grows linearly with time. We also observe that the radius of curvature is directly influenced by :math:`\tau`, as experienced strafers would expect. Namely, we can make sharper turns at higher frame rates. 444 | 445 | From Equation :eq:`radius-speed-relationship` we can derive formulae for various types of strafing by eliminating :math:`\theta`. For instance, in Type 2 strafing we have :math:`\theta = \pi/2`. Substituting, we obtain a very simple expression for the radius: 446 | 447 | .. math:: r \approx \tau \left( \frac{2}{L} \lVert\mathbf{v}\rVert^2 + L \right) 448 | 449 | Or, solving for :math:`\lVert\mathbf{v}\rVert`, we obtain a more useful equation: 450 | 451 | .. math:: \lVert\mathbf{v}\rVert \approx \sqrt{\frac{L}{2} \left( \frac{r}{\tau} - L \right)} 452 | 453 | For Type 1 strafing, the formula is clumsier. Recall that we have :math:`\mu = k_e \tau MA` and 454 | 455 | .. math:: \cos\theta = \frac{L - k_e \tau MA}{\lVert\mathbf{v}\rVert} 456 | 457 | To eliminate :math:`\sin\theta`, we can trivially rewrite the :math:`\cos\theta` equation in this form 458 | 459 | .. math:: \sin\theta = \frac{\sqrt{\lVert\mathbf{v}\rVert^2 - (L - k_e \tau MA)^2}}{\lVert\mathbf{v}\rVert} 460 | 461 | Then we proceed by substituting, yielding 462 | 463 | .. math:: r \approx \frac{\tau \lVert\mathbf{v}\rVert}{\sqrt{\lVert\mathbf{v}\rVert^2 - (L - k_e \tau MA)^2}} \left( \frac{2}{k_e \tau MA} \lVert\mathbf{v}\rVert^2 + 3L - 2 k_e \tau MA \right) 464 | 465 | We cannot simplify this equation further. In fact, solving for :math:`\lVert\mathbf{v}\rVert` is non-trivial as it requires finding a root to a relatively high order polynomial equation. As per the usual strategy when facing similar difficulties, we resort to iterative methods. 466 | -------------------------------------------------------------------------------- /injectlib/.gitignore: -------------------------------------------------------------------------------- 1 | *.o 2 | *.so 3 | -------------------------------------------------------------------------------- /injectlib/Makefile: -------------------------------------------------------------------------------- 1 | CXX = g++ 2 | CXXFLAGS = -O3 -ffast-math -std=c++11 -m32 -march=native -mtune=native -Wall -Wextra -fPIC -flto 3 | OBJS = injectmain.o symutils.o customhud.o movement.o strafemath.o 4 | OUTPUT = tasinjectlib.so 5 | 6 | all: $(OUTPUT) 7 | 8 | $(OUTPUT): $(OBJS) 9 | $(CXX) -shared -s $(CXXFLAGS) $(OBJS) -o $(OUTPUT) 10 | 11 | clean: 12 | rm -f $(OUTPUT) 13 | rm -f *.o 14 | -------------------------------------------------------------------------------- /injectlib/common.hpp: -------------------------------------------------------------------------------- 1 | #ifndef TASCOMMON_H 2 | #define TASCOMMON_H 3 | 4 | #include 5 | 6 | typedef struct cvar_s 7 | { 8 | const char *name; 9 | const char *string; 10 | int flags; 11 | float value; 12 | struct cvar_s *next; 13 | } cvar_t; 14 | 15 | const int FL_DUCKING = 1 << 14; 16 | 17 | typedef void (*Cvar_RegisterVariable_func_t)(cvar_t *); 18 | typedef void (*Cvar_SetValue_func_t)(const char *, float); 19 | typedef void (*GetSetViewAngles_func_t)(float *); 20 | typedef void (*Con_Printf_func_t)(const char *, ...); 21 | typedef const char *(*Cmd_Argv_func_t)(int); 22 | 23 | extern Cvar_SetValue_func_t orig_Cvar_SetValue; 24 | extern Cvar_RegisterVariable_func_t orig_Cvar_RegisterVariable; 25 | extern GetSetViewAngles_func_t orig_GetViewAngles; 26 | extern Con_Printf_func_t orig_Con_Printf; 27 | 28 | extern double *p_host_frametime; 29 | extern uintptr_t *pp_sv_player; 30 | extern const char *game_dir; 31 | extern unsigned int *p_g_ulFrameCount; 32 | extern uintptr_t *pp_gpGlobals; 33 | extern cvar_t sv_taslog; 34 | extern bool mvmt_clipped; 35 | 36 | void abort_with_err(const char *errstr, ...); 37 | 38 | #endif 39 | -------------------------------------------------------------------------------- /injectlib/customhud.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "customhud.hpp" 4 | #include "common.hpp" 5 | 6 | #define ScreenWidth (*(int *)(p_gHUD + 0x1f98 + 0x4)) 7 | #define ScreenHeight (*(int *)(p_gHUD + 0x1f98 + 0x8)) 8 | 9 | struct TraceResult 10 | { 11 | int fAllSolid; 12 | int fStartSolid; 13 | int fInOpen; 14 | int fInWater; 15 | float flFraction; 16 | float vecEndPos[3]; 17 | float flPlaneDist; 18 | float vecPlaneNormal[3]; 19 | uintptr_t pHit; 20 | int iHitgroup; 21 | }; 22 | 23 | typedef struct { 24 | int x, y; 25 | } POSITION; 26 | 27 | class CHudBase 28 | { 29 | public: 30 | POSITION m_pos; 31 | int m_type; 32 | int m_iFlags; 33 | #ifndef OPPOSINGFORCE 34 | virtual ~CHudBase() {} 35 | #endif 36 | virtual int Init(void) { return 0; } 37 | virtual int VidInit(void) { return 0; } 38 | virtual int Draw(float) { return 0; } 39 | virtual void Think(void) { return; } 40 | virtual void Reset(void) { return; } 41 | virtual void InitHUDData(void) {} 42 | }; 43 | 44 | class CHudPlrInfo : CHudBase 45 | { 46 | public: 47 | int Init(); 48 | int Draw(float flTime); 49 | }; 50 | 51 | typedef void (*PF_makevectors_I_func_t)(const float *); 52 | typedef void (*PF_traceline_DLL_func_t)(const float *, const float *, int, uintptr_t, void *); 53 | typedef int (*DrawConsoleString_func_t)(int, int, const char *); 54 | typedef void (*DrawSetTextColor_func_t)(float, float, float); 55 | typedef void (*AddHudElem_func_t)(uintptr_t, void *); 56 | typedef void (*Draw_FillRGBA_func_t)(int, int, int, int, int, int, int, int); 57 | 58 | static uintptr_t p_gHUD = 0; 59 | static uintptr_t p_gEngfuncs = 0; 60 | static CHudPlrInfo hudPlrInfo; 61 | static float default_color[3] = {1.0, 0.7, 0.0}; 62 | 63 | static PF_makevectors_I_func_t orig_PF_makevectors_I = nullptr; 64 | static PF_traceline_DLL_func_t orig_PF_traceline_DLL = nullptr; 65 | static AddHudElem_func_t orig_AddHudElem = nullptr; 66 | static DrawConsoleString_func_t orig_DrawConsoleString = nullptr; 67 | static DrawSetTextColor_func_t orig_DrawSetTextColor = nullptr; 68 | static Draw_FillRGBA_func_t orig_Draw_FillRGBA = nullptr; 69 | 70 | static float get_entity_health() 71 | { 72 | float *plrorigin = (float *)(*pp_sv_player + 0x80 + 0x8); 73 | float *viewofs = (float *)(*pp_sv_player + 0x80 + 0x174); 74 | float start[3] = {plrorigin[0] + viewofs[0], plrorigin[1] + viewofs[1], 75 | plrorigin[2] + viewofs[2]}; 76 | orig_PF_makevectors_I((float *)(*pp_sv_player + 0x80 + 0x74)); 77 | float *g_forward = (float *)(*pp_gpGlobals + 0x28); 78 | float end[3]; 79 | for (int i = 0; i < 3; i++) 80 | end[i] = plrorigin[i] + 8192 * g_forward[i]; 81 | 82 | TraceResult trace; 83 | orig_PF_traceline_DLL(start, end, 0, *pp_sv_player, &trace); 84 | return *(float *)(trace.pHit + 0x80 + 0x160); 85 | } 86 | 87 | static void draw_blocked(float flTime) 88 | { 89 | const int BOX_WIDTH = 50; 90 | const float BOX_DURATION = 0.1; 91 | static float prev_time = 0; 92 | 93 | if (mvmt_clipped) 94 | prev_time = flTime; 95 | 96 | float timediff = flTime - prev_time; 97 | if (timediff < 0 || timediff >= BOX_DURATION) 98 | return; 99 | 100 | orig_Draw_FillRGBA((ScreenWidth - BOX_WIDTH) / 2, 101 | (ScreenHeight - BOX_WIDTH - 10), 102 | BOX_WIDTH, BOX_WIDTH, 255, 255, 0, 103 | 255 * (1 - timediff / BOX_DURATION)); 104 | } 105 | 106 | int CHudPlrInfo::Init() 107 | { 108 | m_iFlags |= 1; 109 | orig_AddHudElem(p_gHUD, this); 110 | return 1; 111 | } 112 | 113 | int CHudPlrInfo::Draw(float flTime) 114 | { 115 | orig_DrawSetTextColor(default_color[0], default_color[1], 116 | default_color[2]); 117 | 118 | char dispstr[30]; 119 | 120 | float *vel = (float *)(*pp_sv_player + 0x80 + 0x20); 121 | snprintf(dispstr, sizeof(dispstr), "H: %.8g\n", 122 | std::hypot(vel[0], vel[1])); 123 | orig_DrawConsoleString(10, 20, dispstr); 124 | snprintf(dispstr, sizeof(dispstr), "V: %.8g\n", vel[2]); 125 | orig_DrawConsoleString(10, 30, dispstr); 126 | 127 | static float prev_origin[3] = {0, 0, 0}; 128 | float *origin = (float *)(*pp_sv_player + 0x80 + 0x8); 129 | float dvel[3]; 130 | for (int i = 0; i < 3; i++) { 131 | dvel[i] = (origin[i] - prev_origin[i]) / *p_host_frametime; 132 | prev_origin[i] = origin[i]; 133 | } 134 | snprintf(dispstr, sizeof(dispstr), "AH: %.8g\n", 135 | std::hypot(dvel[0], dvel[1])); 136 | orig_DrawConsoleString(10, 40, dispstr); 137 | snprintf(dispstr, sizeof(dispstr), "AV: %.8g\n", dvel[2]); 138 | orig_DrawConsoleString(10, 50, dispstr); 139 | 140 | float viewangles[3]; 141 | orig_GetViewAngles(viewangles); 142 | snprintf(dispstr, sizeof(dispstr), "Y: %.8g\n", viewangles[1]); 143 | orig_DrawConsoleString(10, 60, dispstr); 144 | snprintf(dispstr, sizeof(dispstr), "P: %.8g\n", viewangles[0]); 145 | orig_DrawConsoleString(10, 70, dispstr); 146 | 147 | float health = *(float *)(*pp_sv_player + 0x80 + 0x160); 148 | snprintf(dispstr, sizeof(dispstr), "HP: %.8g\n", health); 149 | orig_DrawConsoleString(10, 80, dispstr); 150 | 151 | float ent_hp = get_entity_health(); 152 | snprintf(dispstr, sizeof(dispstr), "EHP: %.8g\n", ent_hp); 153 | orig_DrawConsoleString(10, 90, dispstr); 154 | 155 | int ducked = *(int *)(*pp_sv_player + 0x80 + 0x1a4) & FL_DUCKING; 156 | if (ducked) 157 | orig_DrawSetTextColor(1, 0, 1); 158 | orig_DrawConsoleString(10, 100, ducked ? "ducked" : "standing"); 159 | if (ducked) 160 | orig_DrawSetTextColor(default_color[0], default_color[1], 161 | default_color[2]); 162 | 163 | draw_blocked(flTime); 164 | 165 | return 1; 166 | } 167 | 168 | void initialize_customhud(uintptr_t clso_addr, const symtbl_t &clso_st, 169 | uintptr_t hwso_addr, const symtbl_t &hwso_st) 170 | { 171 | orig_PF_makevectors_I = (PF_makevectors_I_func_t)(hwso_addr + hwso_st.at("PF_makevectors_I")); 172 | orig_PF_traceline_DLL = (PF_traceline_DLL_func_t)(hwso_addr + hwso_st.at("PF_traceline_DLL")); 173 | orig_Draw_FillRGBA = (Draw_FillRGBA_func_t)(hwso_addr + hwso_st.at("Draw_FillRGBA")); 174 | p_gHUD = (uintptr_t)(clso_addr + clso_st.at("gHUD")); 175 | p_gEngfuncs = (uintptr_t)(clso_addr + clso_st.at("gEngfuncs")); 176 | orig_AddHudElem = (AddHudElem_func_t)(clso_addr + clso_st.at("_ZN4CHud10AddHudElemEP8CHudBase")); 177 | orig_DrawConsoleString = *(DrawConsoleString_func_t *)(p_gEngfuncs + 0x6c); 178 | orig_DrawSetTextColor = *(DrawSetTextColor_func_t *)(p_gEngfuncs + 0x70); 179 | hudPlrInfo.Init(); 180 | } 181 | -------------------------------------------------------------------------------- /injectlib/customhud.hpp: -------------------------------------------------------------------------------- 1 | #ifndef CUSTOMHUD_H 2 | #define CUSTOMHUD_H 3 | 4 | #include "symutils.hpp" 5 | 6 | void initialize_customhud(uintptr_t clso_addr, const symtbl_t &clso_st, 7 | uintptr_t hwso_addr, const symtbl_t &hwso_st); 8 | 9 | #endif 10 | -------------------------------------------------------------------------------- /injectlib/injectmain.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #ifdef OPPOSINGFORCE 7 | #include 8 | #endif 9 | #include "symutils.hpp" 10 | #include "common.hpp" 11 | #include "movement.hpp" 12 | #include "customhud.hpp" 13 | 14 | #ifdef OPPOSINGFORCE 15 | #define HLSO_NAME "opfor.so" 16 | #else 17 | #define HLSO_NAME "hl.so" 18 | #endif 19 | 20 | struct edict_s; 21 | struct entity_state_s; 22 | struct KeyValueData_s; 23 | struct entvars_s; 24 | 25 | class CWorld 26 | { 27 | void KeyValue(KeyValueData_s *keydat); 28 | }; 29 | 30 | class CBasePlayer 31 | { 32 | int TakeDamage(entvars_s *, entvars_s *, float, int); 33 | }; 34 | 35 | class CGauss 36 | { 37 | void StartFire(); 38 | void PrimaryAttack(); 39 | }; 40 | 41 | class CBasePlayerWeapon 42 | { 43 | int DefaultDeploy(char *, char *, int, char *, int, int); 44 | }; 45 | 46 | #ifdef OPPOSINGFORCE 47 | typedef void *(*dlsym_func_t)(void *, const char *); 48 | #endif 49 | typedef void (*GameDLLInit_func_t)(); 50 | typedef int (*AddToFullPack_func_t)(entity_state_s *, int, edict_s *, edict_s *, int, int, unsigned char *); 51 | typedef void (*PM_Move_func_t)(uintptr_t, int); 52 | typedef int (*PM_FlyMove_func_t)(); 53 | typedef void (*PM_WalkMove_func_t)(); 54 | typedef void (*SCR_UpdateScreen_func_t)(); 55 | typedef void (*InitInput_func_t)(); 56 | typedef void (*SV_SendClientMessages_func_t)(); 57 | typedef uintptr_t (*SZ_GetSpace_func_t)(uintptr_t, int); 58 | typedef void (*PlayerPreThink_func_t)(edict_s *); 59 | typedef void (*CWorld_KeyValue_func_t)(CWorld *, KeyValueData_s *); 60 | typedef int (*CBasePlayer_TakeDamage_func_t)(CBasePlayer *, entvars_s *, entvars_s *, float, int); 61 | typedef void (*Cmd_AddGameCommand_func_t)(const char *, void (*)()); 62 | typedef void (*CGauss_StartFire_func_t)(CGauss *); 63 | typedef void (*CGauss_PrimaryAttack_func_t)(CGauss *); 64 | typedef int (*CBasePlayerWeapon_DefaultDeploy_func_t)(CBasePlayerWeapon *, char *, char *, int, char *, int, int); 65 | 66 | static uintptr_t hwso_addr = 0; 67 | static uintptr_t hlso_addr = 0; 68 | static uintptr_t clso_addr = 0; 69 | static symtbl_t hwso_st; 70 | static symtbl_t hlso_st; 71 | static symtbl_t clso_st; 72 | static cvar_t sv_show_hidents; 73 | static cvar_t sv_show_triggers; 74 | static cvar_t sv_sim_qg; 75 | static cvar_t sv_sim_qws; 76 | static cvar_t sv_sim_grf; 77 | static int in_walkmove = 0; 78 | static int flymove_numtouches[2]; 79 | static float flymove_vel1[3]; 80 | static float flymove_pos1[3]; 81 | 82 | bool mvmt_clipped = false; 83 | cvar_t sv_taslog; 84 | bool tas_hook_initialized = false; 85 | 86 | Cvar_RegisterVariable_func_t orig_Cvar_RegisterVariable = nullptr; 87 | Cvar_SetValue_func_t orig_Cvar_SetValue = nullptr; 88 | Con_Printf_func_t orig_Con_Printf = nullptr; 89 | 90 | double *p_host_frametime = nullptr; 91 | uintptr_t *pp_sv_player = nullptr; 92 | const char *gamedir = nullptr; 93 | unsigned int *p_g_ulFrameCount = nullptr; 94 | uintptr_t *pp_gpGlobals = nullptr; 95 | 96 | #ifdef OPPOSINGFORCE 97 | static dlsym_func_t orig_dlsym = nullptr; 98 | #endif 99 | static GameDLLInit_func_t orig_GameDLLInit = nullptr; 100 | static AddToFullPack_func_t orig_AddToFullPack = nullptr; 101 | static InitInput_func_t orig_InitInput = nullptr; 102 | static SCR_UpdateScreen_func_t orig_SCR_UpdateScreen = nullptr; 103 | static PM_Move_func_t orig_hl_PM_Move = nullptr; 104 | static PM_Move_func_t orig_cl_PM_Move = nullptr; 105 | static PM_FlyMove_func_t orig_hl_PM_FlyMove = nullptr; 106 | static PM_FlyMove_func_t orig_cl_PM_FlyMove = nullptr; 107 | static PM_WalkMove_func_t orig_hl_PM_WalkMove = nullptr; 108 | static PM_WalkMove_func_t orig_cl_PM_WalkMove = nullptr; 109 | static SV_SendClientMessages_func_t orig_SV_SendClientMessages = nullptr; 110 | static SZ_GetSpace_func_t orig_SZ_GetSpace = nullptr; 111 | static PlayerPreThink_func_t orig_PlayerPreThink = nullptr; 112 | static CWorld_KeyValue_func_t orig_CWorld_KeyValue = nullptr; 113 | static CBasePlayer_TakeDamage_func_t orig_CBasePlayer_TakeDamage = nullptr; 114 | static CGauss_StartFire_func_t orig_hl_CGauss_StartFire = nullptr; 115 | static CGauss_StartFire_func_t orig_cl_CGauss_StartFire = nullptr; 116 | static CGauss_PrimaryAttack_func_t orig_hl_CGauss_PrimaryAttack = nullptr; 117 | static CGauss_PrimaryAttack_func_t orig_cl_CGauss_PrimaryAttack = nullptr; 118 | static Cmd_AddGameCommand_func_t orig_Cmd_AddGameCommand = nullptr; 119 | static Cmd_Argv_func_t orig_Cmd_Argv = nullptr; 120 | static CBasePlayerWeapon_DefaultDeploy_func_t orig_hl_CBasePlayerWeapon_DefaultDeploy = nullptr; 121 | static CBasePlayerWeapon_DefaultDeploy_func_t orig_cl_CBasePlayerWeapon_DefaultDeploy = nullptr; 122 | 123 | static cvar_t *p_r_norefresh = nullptr; 124 | static uintptr_t p_pmove = 0; 125 | static int *p_g_onladder = nullptr; 126 | static uintptr_t p_g_Gauss = 0; 127 | 128 | static const int EF_NODRAW = 128; 129 | static const int kRenderNormal = 0; 130 | static const int kRenderTransColor = 1; 131 | static const int kRenderFxPulseFastWide = 4; 132 | 133 | void abort_with_err(const char *errstr, ...) 134 | { 135 | va_list args; 136 | va_start(args, errstr); 137 | std::fprintf(stderr, "TAS ERROR: "); 138 | std::vfprintf(stderr, errstr, args); 139 | va_end(args); 140 | std::abort(); 141 | } 142 | 143 | static inline const char *hlname_to_string(unsigned int name) 144 | { 145 | return (const char *)(*(uintptr_t *)(*pp_gpGlobals + 0x98) + name); 146 | } 147 | 148 | static void load_cl_symbols() 149 | { 150 | std::string clso_fullpath; 151 | get_loaded_lib_info("client.so", clso_addr, clso_fullpath); 152 | if (!clso_addr) 153 | abort_with_err("Failed to get the base address of client.so."); 154 | clso_st = get_symbols(clso_fullpath.c_str()); 155 | 156 | orig_cl_PM_Move = (PM_Move_func_t)(clso_addr + clso_st["PM_Move"]); 157 | orig_cl_PM_FlyMove = (PM_FlyMove_func_t)(clso_addr + clso_st["PM_FlyMove"]); 158 | orig_cl_PM_WalkMove = (PM_WalkMove_func_t)(clso_addr + clso_st["PM_WalkMove"]); 159 | orig_InitInput = (InitInput_func_t)(clso_addr + clso_st["_Z9InitInputv"]); 160 | orig_cl_CGauss_StartFire = (CGauss_StartFire_func_t)(clso_addr + clso_st["_ZN6CGauss9StartFireEv"]); 161 | orig_cl_CGauss_PrimaryAttack = (CGauss_PrimaryAttack_func_t)(clso_addr + clso_st["_ZN6CGauss13PrimaryAttackEv"]); 162 | orig_cl_CBasePlayerWeapon_DefaultDeploy = (CBasePlayerWeapon_DefaultDeploy_func_t)(clso_addr + clso_st["_ZN17CBasePlayerWeapon13DefaultDeployEPcS0_iS0_ii"]); 163 | p_g_Gauss = clso_addr + clso_st["g_Gauss"]; 164 | } 165 | 166 | static void load_hl_symbols() 167 | { 168 | std::string hlso_fullpath; 169 | get_loaded_lib_info(HLSO_NAME, hlso_addr, hlso_fullpath); 170 | if (!hlso_addr) 171 | abort_with_err("Failed to get the base address of %s.", HLSO_NAME); 172 | hlso_st = get_symbols(hlso_fullpath.c_str()); 173 | 174 | orig_hl_PM_Move = (PM_Move_func_t)(hlso_addr + hlso_st["PM_Move"]); 175 | orig_hl_PM_FlyMove = (PM_FlyMove_func_t)(hlso_addr + hlso_st["PM_FlyMove"]); 176 | orig_hl_PM_WalkMove = (PM_WalkMove_func_t)(hlso_addr + hlso_st["PM_WalkMove"]); 177 | orig_GameDLLInit = (GameDLLInit_func_t)(hlso_addr + hlso_st["_Z11GameDLLInitv"]); 178 | orig_AddToFullPack = (AddToFullPack_func_t)(hlso_addr + hlso_st["_Z13AddToFullPackP14entity_state_siP7edict_sS2_iiPh"]); 179 | orig_PlayerPreThink = (PlayerPreThink_func_t)(hlso_addr + hlso_st["_Z14PlayerPreThinkP7edict_s"]); 180 | orig_CWorld_KeyValue = (CWorld_KeyValue_func_t)(hlso_addr + hlso_st["_ZN6CWorld8KeyValueEP14KeyValueData_s"]); 181 | orig_CBasePlayer_TakeDamage = (CBasePlayer_TakeDamage_func_t)(hlso_addr + hlso_st["_ZN11CBasePlayer10TakeDamageEP9entvars_sS1_fi"]); 182 | orig_hl_CGauss_StartFire = (CGauss_StartFire_func_t)(hlso_addr + hlso_st["_ZN6CGauss9StartFireEv"]); 183 | orig_hl_CGauss_PrimaryAttack = (CGauss_PrimaryAttack_func_t)(hlso_addr + hlso_st["_ZN6CGauss13PrimaryAttackEv"]); 184 | orig_hl_CBasePlayerWeapon_DefaultDeploy = (CBasePlayerWeapon_DefaultDeploy_func_t)(hlso_addr + hlso_st["_ZN17CBasePlayerWeapon13DefaultDeployEPcS0_iS0_ii"]); 185 | 186 | pp_gpGlobals = (uintptr_t *)(hlso_addr + hlso_st["gpGlobals"]); 187 | p_g_ulFrameCount = (unsigned int *)(hlso_addr + hlso_st["g_ulFrameCount"]); 188 | p_g_onladder = (int *)(hlso_addr + hlso_st["g_onladder"]); 189 | } 190 | 191 | static void load_hw_symbols() 192 | { 193 | std::string hwso_fullpath; 194 | get_loaded_lib_info("hw.so", hwso_addr, hwso_fullpath); 195 | if (!hwso_addr) 196 | abort_with_err("Failed to get the base address of hw.so."); 197 | hwso_st = get_symbols(hwso_fullpath.c_str()); 198 | 199 | orig_Cvar_RegisterVariable = (Cvar_RegisterVariable_func_t)(hwso_addr + hwso_st["Cvar_RegisterVariable"]); 200 | orig_Cvar_SetValue = (Cvar_SetValue_func_t)(hwso_addr + hwso_st["Cvar_SetValue"]); 201 | orig_Cmd_AddGameCommand = (Cmd_AddGameCommand_func_t)(hwso_addr + hwso_st["Cmd_AddGameCommand"]); 202 | orig_Cmd_Argv = (Cmd_Argv_func_t)(hwso_addr + hwso_st["Cmd_Argv"]); 203 | orig_SCR_UpdateScreen = (SCR_UpdateScreen_func_t)(hwso_addr + hwso_st["SCR_UpdateScreen"]); 204 | orig_SV_SendClientMessages = (SV_SendClientMessages_func_t)(hwso_addr + hwso_st["SV_SendClientMessages"]); 205 | orig_SZ_GetSpace = (SZ_GetSpace_func_t)(hwso_addr + hwso_st["SZ_GetSpace"]); 206 | orig_Con_Printf = (Con_Printf_func_t)(hwso_addr + hwso_st["Con_Printf"]); 207 | 208 | gamedir = (const char *)(hwso_addr + hwso_st["com_gamedir"]); 209 | p_host_frametime = (double *)(hwso_addr + hwso_st["host_frametime"]); 210 | pp_sv_player = (uintptr_t *)(hwso_addr + hwso_st["sv_player"]); 211 | p_r_norefresh = (cvar_t *)(hwso_addr + hwso_st["r_norefresh"]); 212 | } 213 | 214 | // Note that this function is called before GameDLLInit. 215 | void InitInput() 216 | { 217 | if (!tas_hook_initialized) { 218 | load_cl_symbols(); 219 | initialize_movement(clso_addr, clso_st, hwso_addr, hwso_st); 220 | } 221 | orig_InitInput(); 222 | } 223 | 224 | static void change_plr_hp() 225 | { 226 | if (!pp_sv_player || !*pp_sv_player) 227 | return; 228 | *(float *)(*pp_sv_player + 0x80 + 0x160) = std::atof(orig_Cmd_Argv(1)); 229 | } 230 | 231 | static void change_plr_ap() 232 | { 233 | if (!pp_sv_player || !*pp_sv_player) 234 | return; 235 | *(float *)(*pp_sv_player + 0x80 + 0x1bc) = std::atof(orig_Cmd_Argv(1)); 236 | } 237 | 238 | void GameDLLInit() 239 | { 240 | if (!tas_hook_initialized) { 241 | // We only initialise the custom HUD here because it will fail to 242 | // initialise if we do it in InitInput instead. 243 | initialize_customhud(clso_addr, clso_st, hwso_addr, hwso_st); 244 | load_hl_symbols(); 245 | orig_Cmd_AddGameCommand("ch_health", change_plr_hp); 246 | orig_Cmd_AddGameCommand("ch_armor", change_plr_ap); 247 | tas_hook_initialized = true; // finally, everything is initialised 248 | } 249 | orig_GameDLLInit(); 250 | } 251 | 252 | static void get_trigger_amt_colors(const char *type, int *amt, char colors[3]) 253 | { 254 | if (strcmp(type, "once") == 0) { 255 | *amt = 120; 256 | colors[0] = 0; 257 | colors[1] = 255; 258 | colors[2] = 255; 259 | } else if (strcmp(type, "multiple") == 0) { 260 | *amt = 120; 261 | colors[0] = 0; 262 | colors[1] = 0; 263 | colors[2] = 255; 264 | } else if (strcmp(type, "changelevel") == 0) { 265 | *amt = 180; 266 | colors[0] = 255; 267 | colors[1] = 0; 268 | colors[2] = 255; 269 | } else if (strcmp(type, "hurt") == 0) { 270 | *amt = 140; 271 | colors[0] = 255; 272 | colors[1] = 0; 273 | colors[2] = 0; 274 | } else if (strcmp(type, "push") == 0) { 275 | *amt = 120; 276 | colors[0] = 255; 277 | colors[1] = 255; 278 | colors[2] = 0; 279 | } else if (strcmp(type, "teleport") == 0) { 280 | *amt = 150; 281 | colors[0] = 0; 282 | colors[1] = 255; 283 | colors[2] = 0; 284 | } else { 285 | *amt = 100; 286 | colors[0] = 255; 287 | colors[1] = 255; 288 | colors[2] = 255; 289 | } 290 | } 291 | 292 | int AddToFullPack(entity_state_s *state, int e, edict_s *ent, edict_s *host, 293 | int hostflags, int player, unsigned char *pSet) 294 | { 295 | uintptr_t entvarsaddr = (uintptr_t)ent + 0x80; 296 | const char *classname = hlname_to_string(*(unsigned int *)entvarsaddr); 297 | bool is_trigger = std::strncmp(classname, "trigger_", 8) == 0; 298 | 299 | if ((!is_trigger || !sv_show_triggers.value) && !sv_show_hidents.value) 300 | return orig_AddToFullPack(state, e, ent, host, hostflags, player, pSet); 301 | 302 | int *p_effects = (int *)(entvarsaddr + 0x118); 303 | int old_effects = *p_effects; 304 | *p_effects &= ~EF_NODRAW; // Trick orig_AddToFullPack 305 | int ret = orig_AddToFullPack(state, e, ent, host, hostflags, player, pSet); 306 | *p_effects = old_effects; 307 | 308 | if (!ret) 309 | return 0; 310 | 311 | uintptr_t stateaddr = (uintptr_t)state; 312 | if (is_trigger && sv_show_triggers.value) { 313 | *(int *)(stateaddr + 0x3c) &= ~EF_NODRAW; 314 | *(int *)(stateaddr + 0x48) = kRenderTransColor; 315 | *(int *)(stateaddr + 0x54) = kRenderFxPulseFastWide; 316 | get_trigger_amt_colors(classname + 8, (int *)(stateaddr + 0x4c), 317 | (char *)(stateaddr + 0x50)); 318 | } else if (sv_show_hidents.value) { 319 | *(int *)(stateaddr + 0x3c) &= ~EF_NODRAW; 320 | *(int *)(stateaddr + 0x48) = kRenderNormal; 321 | } 322 | 323 | return 1; 324 | } 325 | 326 | void PlayerPreThink(edict_s *ent) 327 | { 328 | if (sv_taslog.value) { 329 | orig_Con_Printf("prethink %u %.8g\n", *p_g_ulFrameCount, 330 | *(float *)(*pp_gpGlobals + 0x4)); 331 | orig_Con_Printf("health %.8g %.8g\n", 332 | *(float *)((uintptr_t)ent + 0x80 + 0x160), 333 | *(float *)((uintptr_t)ent + 0x80 + 0x1bc)); 334 | } 335 | orig_PlayerPreThink(ent); 336 | } 337 | 338 | extern "C" void SV_SendClientMessages() 339 | { 340 | if (p_r_norefresh->value <= 2) 341 | orig_SV_SendClientMessages(); 342 | } 343 | 344 | extern "C" uintptr_t SZ_GetSpace(uintptr_t buf, int len) 345 | { 346 | if (p_r_norefresh->value > 2 && 347 | *(int *)(buf + 0x10) + len > *(int *)(buf + 0xc)) { 348 | *(int *)(buf + 0x10) = 0; 349 | } 350 | return orig_SZ_GetSpace(buf, len); 351 | } 352 | 353 | extern "C" void SCR_UpdateScreen() 354 | { 355 | if (p_r_norefresh->value <= 1) 356 | orig_SCR_UpdateScreen(); 357 | } 358 | 359 | extern "C" void Cvar_Init() 360 | { 361 | // We don't load symbols from hl.so here because the game has not loaded 362 | // hl.so into memory. 363 | if (!tas_hook_initialized) 364 | load_hw_symbols(); 365 | 366 | sv_show_triggers.name = "sv_show_triggers"; 367 | sv_show_triggers.string = "0"; 368 | orig_Cvar_RegisterVariable(&sv_show_triggers); 369 | 370 | sv_show_hidents.name = "sv_show_hidents"; 371 | sv_show_hidents.string = "0"; 372 | orig_Cvar_RegisterVariable(&sv_show_hidents); 373 | 374 | sv_taslog.name = "sv_taslog"; 375 | sv_taslog.string = "0"; 376 | orig_Cvar_RegisterVariable(&sv_taslog); 377 | 378 | sv_sim_qg.name = "sv_sim_qg"; 379 | sv_sim_qg.string = "0"; 380 | orig_Cvar_RegisterVariable(&sv_sim_qg); 381 | 382 | sv_sim_qws.name = "sv_sim_qws"; 383 | sv_sim_qws.string = "0"; 384 | orig_Cvar_RegisterVariable(&sv_sim_qws); 385 | 386 | sv_sim_grf.name = "sv_sim_grf"; 387 | sv_sim_grf.string = "0"; 388 | orig_Cvar_RegisterVariable(&sv_sim_grf); 389 | } 390 | 391 | static void print_tasinfo(uintptr_t pmove, int server, int num) 392 | { 393 | if (!server || !sv_taslog.value) 394 | return; 395 | 396 | if (num == 1) { 397 | uintptr_t cmd = pmove + 0x45458; 398 | orig_Con_Printf("usercmd %d %u %.8g %.8g\n", 399 | *(char *)(cmd + 0x2), *(unsigned short *)(cmd + 0x1e), 400 | *(float *)(cmd + 0x4), *(float *)(cmd + 0x8)); 401 | orig_Con_Printf("fsu %.8g %.8g %.8g\n", 402 | *(float *)(cmd + 0x10), *(float *)(cmd + 0x14), 403 | *(float *)(cmd + 0x18)); 404 | orig_Con_Printf("fg %.8g %.8g\n", *(float *)(pmove + 0xc4), 405 | *(float *)(pmove + 0xc0)); 406 | orig_Con_Printf("pa %.8g %.8g\n", *(float *)(pmove + 0xa0), 407 | *(float *)(pmove + 0xa4)); 408 | } else if (num == 2) 409 | orig_Con_Printf("ntl %d %d\n", mvmt_clipped, *p_g_onladder); 410 | 411 | float *pos = (float *)(pmove + 0x38); 412 | orig_Con_Printf("pos %d %.8g %.8g %.8g\n", num, pos[0], pos[1], pos[2]); 413 | 414 | float *vel = (float *)(pmove + 0x5c); 415 | float *basevel = (float *)(pmove + 0x74); 416 | orig_Con_Printf("pmove %d %.8g %.8g %.8g %.8g %.8g %.8g %d %u %d %d\n", 417 | num, vel[0], vel[1], vel[2], 418 | basevel[0], basevel[1], basevel[2], 419 | *(int *)(pmove + 0x90), *(unsigned int *)(pmove + 0xb8), 420 | *(int *)(pmove + 0xe0), *(int *)(pmove + 0xe4)); 421 | } 422 | 423 | extern "C" void PM_Move(uintptr_t ppmove, int server) 424 | { 425 | p_pmove = ppmove; 426 | mvmt_clipped = false; 427 | print_tasinfo(ppmove, server, 1); 428 | (server ? orig_hl_PM_Move : orig_cl_PM_Move)(ppmove, server); 429 | print_tasinfo(ppmove, server, 2); 430 | } 431 | 432 | extern "C" int PM_FlyMove() 433 | { 434 | if (!*(int *)(p_pmove + 0x4)) 435 | return orig_cl_PM_FlyMove(); 436 | 437 | const int *p_numtouch = (const int *)(p_pmove + 0x4548c); 438 | int old_numtouch = *p_numtouch; 439 | int ret = orig_hl_PM_FlyMove(); 440 | if (!in_walkmove) { 441 | mvmt_clipped = *p_numtouch - old_numtouch; 442 | return ret; 443 | } 444 | 445 | flymove_numtouches[in_walkmove - 1] = *p_numtouch - old_numtouch; 446 | if (in_walkmove == 1) { 447 | for (int i = 0; i < 3; i++) { 448 | flymove_vel1[i] = ((float *)(p_pmove + 0x5c))[i]; 449 | flymove_pos1[i] =((float *)(p_pmove + 0x38))[i]; 450 | } 451 | } 452 | 453 | in_walkmove++; 454 | return ret; 455 | } 456 | 457 | extern "C" void PM_WalkMove() 458 | { 459 | if (!*(int *)(p_pmove + 0x4)) { 460 | orig_cl_PM_WalkMove(); 461 | return; 462 | } 463 | 464 | in_walkmove = 1; 465 | orig_hl_PM_WalkMove(); 466 | if (in_walkmove == 1) { // if PM_FlyMove wasn't called 467 | mvmt_clipped = 0; 468 | in_walkmove = 0; 469 | return; 470 | } 471 | in_walkmove = 0; 472 | 473 | float *vel = (float *)(p_pmove + 0x5c); 474 | float *origin = (float *)(p_pmove + 0x38); 475 | 476 | if (vel[0] == flymove_vel1[0] && vel[1] == flymove_vel1[1] && 477 | vel[2] == flymove_vel1[2] && origin[0] == flymove_pos1[0] && 478 | origin[1] == flymove_pos1[1] && origin[2] == flymove_pos1[2]) 479 | mvmt_clipped = flymove_numtouches[0]; 480 | else 481 | mvmt_clipped = flymove_numtouches[1]; 482 | } 483 | 484 | void CWorld::KeyValue(KeyValueData_s *keydat) 485 | { 486 | char *keystr = *(char **)((uintptr_t)keydat + 4); 487 | if (strcmp(keystr, "startdark") == 0) 488 | return; 489 | orig_CWorld_KeyValue(this, keydat); 490 | } 491 | 492 | int CBasePlayer::TakeDamage(entvars_s *pevInflictor, entvars_s *pevAttacker, 493 | float flDamage, int bitsDamageType) 494 | { 495 | if (sv_taslog.value) 496 | orig_Con_Printf("dmg %.8g %d\n", flDamage, bitsDamageType); 497 | return orig_CBasePlayer_TakeDamage(this, pevInflictor, pevAttacker, 498 | flDamage, bitsDamageType); 499 | } 500 | 501 | static inline void call_orig_startfire(CGauss *cgauss) 502 | { 503 | if ((uintptr_t)cgauss == p_g_Gauss) 504 | orig_cl_CGauss_StartFire(cgauss); 505 | else 506 | orig_hl_CGauss_StartFire(cgauss); 507 | } 508 | 509 | void CGauss::StartFire() 510 | { 511 | if (!sv_sim_qg.value) { 512 | call_orig_startfire(this); 513 | return; 514 | } 515 | 516 | uintptr_t p_player = *(uintptr_t *)((uintptr_t)this + 0x80); 517 | float *p_start_charge = (float *)(p_player + 0x640); 518 | float old_start_charge = *p_start_charge; 519 | *p_start_charge = 0; 520 | call_orig_startfire(this); 521 | *p_start_charge = old_start_charge; 522 | } 523 | 524 | void CGauss::PrimaryAttack() 525 | { 526 | if ((uintptr_t)this == p_g_Gauss) 527 | orig_cl_CGauss_PrimaryAttack(this); 528 | else 529 | orig_hl_CGauss_PrimaryAttack(this); 530 | 531 | if (sv_sim_grf.value) { 532 | uintptr_t thisplayer = *(uintptr_t *)((uintptr_t)this + 0x80); 533 | *(float *)(thisplayer + 0x264) = 0; 534 | } 535 | } 536 | 537 | int CBasePlayerWeapon::DefaultDeploy(char *szViewModel, char *szWeaponModel, 538 | int iAnim, char *szAnimExt, int skiplocal, 539 | int body) 540 | { 541 | uintptr_t clplayer = *(uintptr_t *)(p_g_Gauss + 0x80); 542 | uintptr_t thisplayer = *(uintptr_t *)((uintptr_t)this + 0x80); 543 | int ret = (thisplayer == clplayer ? 544 | orig_cl_CBasePlayerWeapon_DefaultDeploy : 545 | orig_hl_CBasePlayerWeapon_DefaultDeploy)( 546 | this, szViewModel, szWeaponModel, iAnim, 547 | szAnimExt, skiplocal, body); 548 | if (sv_sim_qws.value) 549 | *(float *)(thisplayer + 0x264) = 0; 550 | return ret; 551 | } 552 | 553 | #ifdef OPPOSINGFORCE 554 | extern "C" void CL_CreateMove(float, void *, int); 555 | 556 | extern "C" void *dlsym(void *handle, const char *symbol) 557 | { 558 | if (symbol && strcmp(symbol, "CL_CreateMove") == 0) 559 | return (void *)CL_CreateMove; 560 | return orig_dlsym(handle, symbol); 561 | } 562 | 563 | static __attribute__((constructor)) void Constructor() 564 | { 565 | std::string libdl_fullpath; 566 | uintptr_t libdl_addr; 567 | symtbl_t libdl_symbols; 568 | get_loaded_lib_info("libdl.so.2", libdl_addr, libdl_fullpath); 569 | libdl_symbols = get_symbols(libdl_fullpath.c_str()); 570 | for (auto it = libdl_symbols.begin(); it != libdl_symbols.end(); ++it) { 571 | if (std::strncmp(it->first.c_str(), "dlsym", 5) == 0) { 572 | orig_dlsym = (dlsym_func_t)(libdl_addr + it->second); 573 | break; 574 | } 575 | } 576 | } 577 | #endif 578 | -------------------------------------------------------------------------------- /injectlib/movement.hpp: -------------------------------------------------------------------------------- 1 | #ifndef MOVEMENT_H 2 | #define MOVEMENT_H 3 | 4 | #include "symutils.hpp" 5 | 6 | void initialize_movement(uintptr_t clso_addr, const symtbl_t &clso_st, 7 | uintptr_t hwso_addr, const symtbl_t &hwso_st); 8 | 9 | #endif 10 | -------------------------------------------------------------------------------- /injectlib/strafemath.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "strafemath.hpp" 3 | 4 | double anglemod_deg(double a) 5 | { 6 | return M_U_DEG * ((int)(a / M_U_DEG) & 0xffff); 7 | } 8 | 9 | double anglemod_rad(double a) 10 | { 11 | return M_U_RAD * ((int)(a / M_U_RAD) & 0xffff); 12 | } 13 | 14 | static double point2line_distsq(const double pos[2], 15 | const double line_origin[2], 16 | const double line_dir[2]) 17 | { 18 | double tmp[2] = {line_origin[0] - pos[0], line_origin[1] - pos[1]}; 19 | double dotprod = line_dir[0] * tmp[0] + line_dir[1] * tmp[1]; 20 | tmp[0] -= line_dir[0] * dotprod; 21 | tmp[1] -= line_dir[1] * dotprod; 22 | return tmp[0] * tmp[0] + tmp[1] * tmp[1]; 23 | } 24 | 25 | static double strafe_theta_opt(double speed, double L, double tauMA) 26 | { 27 | double tmp = L - tauMA; 28 | if (tmp <= 0) 29 | return M_PI_2; 30 | if (tmp < speed) 31 | return std::acos(tmp / speed); 32 | return 0; 33 | } 34 | 35 | static double strafe_theta_const(double speed, double nofric_speed, double L, 36 | double tauMA) 37 | { 38 | double sqdiff = nofric_speed * nofric_speed - speed * speed; 39 | double tmp = sqdiff / tauMA; 40 | if (tmp + tauMA < 2 * L && 2 * speed >= std::fabs(tmp - tauMA)) 41 | return std::acos((tmp - tauMA) / (2 * speed)); 42 | tmp = std::sqrt(L * L - sqdiff); 43 | if (tauMA - L > tmp && speed >= tmp) 44 | return std::acos(-tmp / speed); 45 | return strafe_theta_opt(speed, L, tauMA); 46 | } 47 | 48 | void strafe_fme_vec(double vel[2], const double avec[2], double L, 49 | double tauMA) 50 | { 51 | double tmp = L - vel[0] * avec[0] - vel[1] * avec[1]; 52 | if (tmp < 0) 53 | return; 54 | if (tauMA < tmp) 55 | tmp = tauMA; 56 | vel[0] += avec[0] * tmp; 57 | vel[1] += avec[1] * tmp; 58 | } 59 | 60 | void strafe_fric(double vel[2], double E, double ktau) 61 | { 62 | double speed = std::hypot(vel[0], vel[1]); 63 | if (speed >= E) { 64 | vel[0] *= 1 - ktau; 65 | vel[1] *= 1 - ktau; 66 | return; 67 | } 68 | 69 | double tmp = E * ktau; 70 | if (speed > tmp) { 71 | tmp /= speed; 72 | vel[0] -= tmp * vel[0]; 73 | vel[1] -= tmp * vel[1]; 74 | return; 75 | } 76 | 77 | vel[0] = 0; 78 | vel[1] = 0; 79 | } 80 | 81 | double strafe_fric_spd(double spd, double E, double ktau) 82 | { 83 | if (spd >= E) 84 | return spd * (1 - ktau); 85 | double tmp = E * ktau; 86 | if (spd > tmp) 87 | return spd - tmp; 88 | return 0; 89 | } 90 | 91 | static void strafe_side(double &yaw, int &Sdir, int &Fdir, double vel[2], 92 | double theta, double L, double tauMA, int dir) 93 | { 94 | double phi; 95 | // This is to reduce the overall shaking. 96 | if (theta >= M_PI_2 * 0.75) { 97 | Sdir = dir; 98 | Fdir = 0; 99 | phi = std::copysign(M_PI_2, dir); 100 | } else if (M_PI_2 * 0.25 <= theta && theta <= M_PI_2 * 0.75) { 101 | Sdir = dir; 102 | Fdir = 1; 103 | phi = std::copysign(M_PI_4, dir); 104 | } else { 105 | Sdir = 0; 106 | Fdir = 1; 107 | phi = 0; 108 | } 109 | 110 | if (std::fabs(vel[0]) > 0.1 || std::fabs(vel[1]) > 0.1) 111 | yaw = std::atan2(vel[1], vel[0]); 112 | yaw += phi - std::copysign(theta, dir); 113 | double yawcand[2] = { 114 | anglemod_rad(yaw), anglemod_rad(yaw + std::copysign(M_U_RAD, yaw)) 115 | }; 116 | double avec[2] = {std::cos(yawcand[0] - phi), std::sin(yawcand[0] - phi)}; 117 | double tmpvel[2] = {vel[0], vel[1]}; 118 | strafe_fme_vec(vel, avec, L, tauMA); 119 | avec[0] = std::cos(yawcand[1] - phi); 120 | avec[1] = std::sin(yawcand[1] - phi); 121 | strafe_fme_vec(tmpvel, avec, L, tauMA); 122 | 123 | if (tmpvel[0] * tmpvel[0] + tmpvel[1] * tmpvel[1] > 124 | vel[0] * vel[0] + vel[1] * vel[1]) { 125 | vel[0] = tmpvel[0]; 126 | vel[1] = tmpvel[1]; 127 | yaw = yawcand[1]; 128 | } else 129 | yaw = yawcand[0]; 130 | } 131 | 132 | void strafe_side_opt(double &yaw, int &Sdir, int &Fdir, double vel[2], 133 | double L, double tauMA, int dir) 134 | { 135 | double speed = std::hypot(vel[0], vel[1]); 136 | double theta = strafe_theta_opt(speed, L, tauMA); 137 | strafe_side(yaw, Sdir, Fdir, vel, theta, L, tauMA, dir); 138 | } 139 | 140 | void strafe_side_const(double &yaw, int &Sdir, int &Fdir, double vel[2], 141 | double nofricspd, double L, double tauMA, int dir) 142 | { 143 | double speed = std::hypot(vel[0], vel[1]); 144 | double theta = strafe_theta_const(speed, nofricspd, L, tauMA); 145 | strafe_side(yaw, Sdir, Fdir, vel, theta, L, tauMA, dir); 146 | } 147 | 148 | void strafe_line_opt(double &yaw, int &Sdir, int &Fdir, double vel[2], 149 | const double pos[2], double L, double tau, double MA, 150 | const double line_origin[2], const double line_dir[2]) 151 | { 152 | double tauMA = tau * MA; 153 | double speed = std::hypot(vel[0], vel[1]); 154 | double theta = strafe_theta_opt(speed, L, tauMA); 155 | double ct = std::cos(theta); 156 | double tmp = L - speed * ct; 157 | if (tmp < 0) { 158 | strafe_side(yaw, Sdir, Fdir, vel, theta, L, tauMA, 1); 159 | return; 160 | } 161 | 162 | if (tauMA < tmp) 163 | tmp = tauMA; 164 | tmp /= speed; 165 | double st = std::sin(theta); 166 | double newpos_right[2], newpos_left[2]; 167 | double avec[2]; 168 | 169 | avec[0] = (vel[0] * ct + vel[1] * st) * tmp; 170 | avec[1] = (-vel[0] * st + vel[1] * ct) * tmp; 171 | newpos_right[0] = pos[0] + tau * (vel[0] + avec[0]); 172 | newpos_right[1] = pos[1] + tau * (vel[1] + avec[1]); 173 | 174 | avec[0] = (vel[0] * ct - vel[1] * st) * tmp; 175 | avec[1] = (vel[0] * st + vel[1] * ct) * tmp; 176 | newpos_left[0] = pos[0] + tau * (vel[0] + avec[0]); 177 | newpos_left[1] = pos[1] + tau * (vel[1] + avec[1]); 178 | 179 | bool rightgt = point2line_distsq(newpos_right, line_origin, line_dir) < 180 | point2line_distsq(newpos_left, line_origin, line_dir); 181 | strafe_side(yaw, Sdir, Fdir, vel, theta, L, tauMA, rightgt ? 1 : -1); 182 | } 183 | 184 | void strafe_back(double &yaw, int &Sdir, int &Fdir, double vel[2], 185 | double tauMA) 186 | { 187 | Sdir = 0; 188 | Fdir = -1; 189 | 190 | yaw = std::atan2(vel[1], vel[0]); 191 | float frac = yaw / M_U_RAD; 192 | frac -= std::trunc(frac); 193 | if (frac > 0.5) 194 | yaw += M_U_RAD; 195 | else if (frac < -0.5) 196 | yaw -= M_U_RAD; 197 | yaw = anglemod_rad(yaw); 198 | 199 | double avec[2] = {std::cos(yaw), std::sin(yaw)}; 200 | vel[0] -= tauMA * avec[0]; 201 | vel[1] -= tauMA * avec[1]; 202 | } 203 | 204 | double strafe_opt_spd(double spd, double L, double tauMA) 205 | { 206 | double tmp = L - tauMA; 207 | if (tmp < 0) 208 | return std::sqrt(spd * spd + L * L); 209 | if (tmp < spd) 210 | return std::sqrt(spd * spd + tauMA * (L + tmp)); 211 | return spd + tauMA; 212 | } 213 | -------------------------------------------------------------------------------- /injectlib/strafemath.hpp: -------------------------------------------------------------------------------- 1 | #ifndef STRAFEMATH_H 2 | #define STRAFEMATH_H 3 | 4 | const double M_U_DEG = 360.0 / 65536; 5 | const double M_U_RAD = M_PI / 32768; 6 | 7 | void strafe_fme_vec(double vel[2], const double avec[2], double L, 8 | double tauMA); 9 | 10 | void strafe_side_opt(double &yaw, int &Sdir, int &Fdir, double vel[2], 11 | double L, double tauMA, int dir); 12 | 13 | void strafe_line_opt(double &yaw, int &Sdir, int &Fdir, double vel[2], 14 | const double pos[2], double L, double tau, double MA, 15 | const double line_origin[2], const double line_dir[2]); 16 | 17 | void strafe_side_const(double &yaw, int &Sdir, int &Fdir, double vel[2], 18 | double nofricspd, double L, double tauMA, int dir); 19 | 20 | void strafe_back(double &yaw, int &Sdir, int &Fdir, double vel[2], 21 | double tauMA); 22 | 23 | void strafe_fric(double vel[2], double E, double ktau); 24 | 25 | double strafe_fric_spd(double spd, double E, double ktau); 26 | 27 | double strafe_opt_spd(double spd, double L, double tauMA); 28 | 29 | double anglemod_deg(double a); 30 | double anglemod_rad(double a); 31 | 32 | #endif 33 | -------------------------------------------------------------------------------- /injectlib/symutils.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "symutils.hpp" 5 | 6 | struct libbasearg_t 7 | { 8 | const char *libname; 9 | uintptr_t baseaddr; 10 | std::string fullpath; 11 | }; 12 | 13 | static int phdr_callback(dl_phdr_info *info, size_t, void *data) 14 | { 15 | libbasearg_t *arg = (libbasearg_t *)data; 16 | if (std::strcmp(basename(info->dlpi_name), arg->libname) == 0) { 17 | arg->baseaddr = info->dlpi_addr; 18 | arg->fullpath = info->dlpi_name; 19 | } 20 | return 0; 21 | } 22 | 23 | void get_loaded_lib_info(const char *libname, uintptr_t &addr, 24 | std::string &fullpath) 25 | { 26 | libbasearg_t arg = {libname, 0, ""}; 27 | dl_iterate_phdr(phdr_callback, &arg); 28 | addr = arg.baseaddr; 29 | fullpath = arg.fullpath; 30 | } 31 | 32 | static long get_file_size(std::FILE *file) 33 | { 34 | long orig_pos = std::ftell(file); 35 | std::fseek(file, 0, SEEK_END); 36 | long filesize = std::ftell(file); 37 | std::fseek(file, orig_pos, SEEK_SET); 38 | return filesize; 39 | } 40 | 41 | symtbl_t get_symbols(const char *libpath) 42 | { 43 | symtbl_t sym_straddr_tbl; 44 | std::FILE *libfile = std::fopen(libpath, "r"); 45 | if (!libfile) 46 | return sym_straddr_tbl; 47 | long filesize = get_file_size(libfile); 48 | char *filedat = new char[filesize]; 49 | std::fread(filedat, 1, filesize, libfile); 50 | std::fclose(libfile); 51 | 52 | Elf32_Ehdr *elf_hdr = (Elf32_Ehdr *)filedat; 53 | Elf32_Shdr *sh_hdr = (Elf32_Shdr *)(filedat + elf_hdr->e_shoff); 54 | Elf32_Shdr *sh_shstrhdr = sh_hdr + elf_hdr->e_shstrndx; 55 | char *sh_shstrtab = filedat + sh_shstrhdr->sh_offset; 56 | 57 | int i; 58 | for (i = 0; sh_hdr[i].sh_type != SHT_DYNSYM; i++); 59 | Elf32_Sym *symtab = (Elf32_Sym *)(filedat + sh_hdr[i].sh_offset); 60 | uint64_t st_num_entries = sh_hdr[i].sh_size / sizeof(Elf32_Sym); 61 | 62 | for (i = 0; sh_hdr[i].sh_type != SHT_STRTAB || 63 | strcmp(sh_shstrtab + sh_hdr[i].sh_name, ".dynstr") != 0; i++); 64 | char *sh_strtab = filedat + sh_hdr[i].sh_offset; 65 | 66 | for (uint64_t i = 0; i < st_num_entries; i++) 67 | sym_straddr_tbl[std::string(sh_strtab + symtab[i].st_name)] = 68 | symtab[i].st_value; 69 | 70 | delete[] filedat; 71 | return sym_straddr_tbl; 72 | } 73 | -------------------------------------------------------------------------------- /injectlib/symutils.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SYMUTILS_H 2 | #define SYMUTILS_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | typedef std::unordered_map symtbl_t; 9 | void get_loaded_lib_info(const char *libname, uintptr_t &addr, 10 | std::string &fullpath); 11 | symtbl_t get_symbols(const char *libpath); 12 | 13 | #endif 14 | -------------------------------------------------------------------------------- /utils/qconread/.gitignore: -------------------------------------------------------------------------------- 1 | qconread 2 | moc_* 3 | Makefile 4 | *.o 5 | -------------------------------------------------------------------------------- /utils/qconread/logtablemodel.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include "logtablemodel.h" 10 | 11 | using std::hypot; 12 | using std::atan2; 13 | using std::sqrt; 14 | 15 | static const QHash DMG_STRING = { 16 | {1, "crush"}, {1 << 1, "bullet"}, {1 << 2, "slash"}, {1 << 3, "burn"}, 17 | {1 << 4, "freeze"}, {1 << 5, "fall"}, {1 << 6, "blast"}, {1 << 7, "club"}, 18 | {1 << 8, "shock"}, {1 << 9, "sonic"}, {1 << 10, "energybeam"}, 19 | {1 << 14, "drown"}, {1 << 18, "radiation"}, {1 << 20, "acid"} 20 | }; 21 | 22 | static const float M_RAD2DEG = 180 / M_PI; 23 | 24 | static const unsigned int IN_ATTACK = 1 << 0; 25 | static const unsigned int IN_JUMP = 1 << 1; 26 | static const unsigned int IN_DUCK = 1 << 2; 27 | static const unsigned int IN_FORWARD = 1 << 3; 28 | static const unsigned int IN_BACK = 1 << 4; 29 | static const unsigned int IN_USE = 1 << 5; 30 | static const unsigned int IN_MOVELEFT = 1 << 9; 31 | static const unsigned int IN_MOVERIGHT = 1 << 10; 32 | static const unsigned int IN_ATTACK2 = 1 << 11; 33 | static const unsigned int IN_RELOAD = 1 << 13; 34 | static const unsigned int FL_DUCKING = 1 << 14; 35 | 36 | static const QBrush brushBlack = QBrush(Qt::black); 37 | static const QBrush brushWhite = QBrush(Qt::white); 38 | static const QBrush brushRed = QBrush(Qt::red); 39 | static const QBrush brushMoveRed = QBrush(QColor(255, 100, 100)); 40 | static const QBrush brushLRed = QBrush(QColor(255, 230, 230)); 41 | static const QBrush brushBlue = QBrush(Qt::blue); 42 | static const QBrush brushMoveBlue = QBrush(QColor(100, 100, 255)); 43 | static const QBrush brushDimBlue = QBrush(QColor(200, 200, 255)); 44 | static const QBrush brushOgGreen = QBrush(QColor(150, 255, 150)); 45 | static const QBrush brushDGreen = QBrush(QColor(0, 120, 0)); 46 | static const QBrush brushFrGray = QBrush(Qt::darkGray); 47 | static const QBrush brushDckGray = QBrush(Qt::gray); 48 | static const QBrush brushMagenta = QBrush(QColor(255, 130, 230)); 49 | static const QBrush brushLMagenta = QBrush(QColor(230, 200, 255)); 50 | static const QBrush brushCyan = QBrush(QColor(80, 255, 255)); 51 | static const QBrush brushYellow = QBrush(QColor(255, 230, 100)); 52 | static const QBrush brushBrown = QBrush(QColor(180, 100, 0)); 53 | 54 | LogTableModel::LogTableModel(QObject *parent) 55 | : QAbstractTableModel(parent) 56 | { 57 | italicFont.setItalic(true); 58 | boldFont.setBold(true); 59 | } 60 | 61 | int LogTableModel::columnCount(const QModelIndex &) const 62 | { 63 | return HEAD_LENGTH; 64 | } 65 | 66 | int LogTableModel::rowCount(const QModelIndex &) const 67 | { 68 | return frameNums.length(); 69 | } 70 | 71 | QVariant LogTableModel::data(const QModelIndex &index, int role) const 72 | { 73 | QString basevelStr; 74 | QString objmoveStr; 75 | int duckState; 76 | int waterLevel; 77 | int moveVal; 78 | 79 | switch (role) { 80 | case Qt::DisplayRole: 81 | switch (index.column()) { 82 | case HEAD_FRATE: 83 | return logTableData[index.row()].frate; 84 | case HEAD_MSEC: 85 | return logTableData[index.row()].msec; 86 | case HEAD_HP: 87 | return logTableData[index.row()].hp; 88 | case HEAD_AP: 89 | return logTableData[index.row()].ap; 90 | case HEAD_YAW: 91 | return logTableData[index.row()].yaw; 92 | case HEAD_PITCH: 93 | return logTableData[index.row()].pitch; 94 | case HEAD_POSX: 95 | return logTableData[index.row()].posx; 96 | case HEAD_POSY: 97 | return logTableData[index.row()].posy; 98 | case HEAD_POSZ: 99 | return logTableData[index.row()].posz; 100 | case HEAD_HSPD: 101 | if (hbasevels.contains(index.row())) 102 | return '*' + QString::number(logTableData[index.row()].hspd); 103 | else 104 | return logTableData[index.row()].hspd; 105 | case HEAD_ANG: 106 | if (hbasevels.contains(index.row())) 107 | return '*' + QString::number(logTableData[index.row()].ang); 108 | else 109 | return logTableData[index.row()].ang; 110 | case HEAD_VSPD: 111 | if (vbasevels.contains(index.row())) 112 | return '*' + QString::number(logTableData[index.row()].vspd); 113 | else 114 | return logTableData[index.row()].vspd; 115 | } 116 | return QVariant(); 117 | 118 | case Qt::ForegroundRole: 119 | switch (index.column()) { 120 | case HEAD_HP: 121 | return damages.contains(index.row()) ? brushWhite : brushRed; 122 | case HEAD_AP: 123 | return damages.contains(index.row()) ? brushWhite : brushBlue; 124 | case HEAD_FRATE: 125 | case HEAD_MSEC: 126 | return brushFrGray; 127 | } 128 | break; 129 | 130 | case Qt::BackgroundRole: 131 | switch (index.column()) { 132 | case HEAD_HSPD: 133 | case HEAD_ANG: 134 | case HEAD_VSPD: 135 | if (numtouches.contains(index.row())) 136 | return brushLRed; 137 | break; 138 | case HEAD_DST: 139 | duckState = logTableData[index.row()].dst; 140 | if (duckState == 2) 141 | return brushBlack; 142 | else if (duckState == 1) 143 | return brushDckGray; 144 | break; 145 | case HEAD_DUCK: 146 | if (logTableData[index.row()].buttons & IN_DUCK) 147 | return brushMagenta; 148 | break; 149 | case HEAD_JUMP: 150 | if (logTableData[index.row()].buttons & IN_JUMP) 151 | return brushCyan; 152 | break; 153 | case HEAD_USE: 154 | if (logTableData[index.row()].buttons & IN_USE) 155 | return brushYellow; 156 | break; 157 | case HEAD_ATTACK: 158 | if (logTableData[index.row()].buttons & IN_ATTACK) 159 | return brushYellow; 160 | break; 161 | case HEAD_ATTACK2: 162 | if (logTableData[index.row()].buttons & IN_ATTACK2) 163 | return brushYellow; 164 | break; 165 | case HEAD_RELOAD: 166 | if (logTableData[index.row()].buttons & IN_RELOAD) 167 | return brushYellow; 168 | break; 169 | case HEAD_FMOVE: 170 | moveVal = logTableData[index.row()].fmove; 171 | if (moveVal > 0) 172 | return brushMoveBlue; 173 | else if (moveVal < 0) 174 | return brushMoveRed; 175 | break; 176 | case HEAD_SMOVE: 177 | moveVal = logTableData[index.row()].smove; 178 | if (moveVal > 0) 179 | return brushMoveBlue; 180 | else if (moveVal < 0) 181 | return brushMoveRed; 182 | break; 183 | case HEAD_UMOVE: 184 | moveVal = logTableData[index.row()].umove; 185 | if (moveVal > 0) 186 | return brushMoveBlue; 187 | else if (moveVal < 0) 188 | return brushMoveRed; 189 | break; 190 | case HEAD_PITCH: 191 | if (punchangles.contains(index.row()) && 192 | punchangles[index.row()].first) 193 | return brushLMagenta; 194 | break; 195 | case HEAD_YAW: 196 | if (punchangles.contains(index.row()) && 197 | punchangles[index.row()].second) 198 | return brushLMagenta; 199 | break; 200 | case HEAD_HP: 201 | case HEAD_AP: 202 | if (damages.contains(index.row())) 203 | return brushRed; 204 | break; 205 | case HEAD_OG: 206 | if (logTableData[index.row()].og) 207 | return brushOgGreen; 208 | break; 209 | case HEAD_WLVL: 210 | waterLevel = logTableData[index.row()].wlvl; 211 | if (waterLevel >= 2) 212 | return brushBlue; 213 | else if (waterLevel == 1) 214 | return brushDimBlue; 215 | break; 216 | case HEAD_LADDER: 217 | if (logTableData[index.row()].ladder) 218 | return brushBrown; 219 | } 220 | break; 221 | 222 | case Qt::FontRole: 223 | switch (index.column()) { 224 | case HEAD_HSPD: 225 | case HEAD_ANG: 226 | if (objmoves.contains(index.row())) 227 | return boldFont; 228 | break; 229 | case HEAD_FMOVE: 230 | case HEAD_SMOVE: 231 | case HEAD_UMOVE: 232 | return boldFont; 233 | case HEAD_HP: 234 | case HEAD_AP: 235 | if (damages.contains(index.row())) 236 | return boldFont; 237 | break; 238 | } 239 | break; 240 | 241 | case Qt::StatusTipRole: 242 | switch (index.column()) { 243 | case HEAD_HSPD: 244 | case HEAD_ANG: 245 | if (hbasevels.contains(index.row())) { 246 | auto entry = hbasevels[index.row()]; 247 | basevelStr = QString("(with basevel) hspd = %1, ang = %2") 248 | .arg(entry.first).arg(entry.second); 249 | } 250 | if (objmoves.contains(index.row())) { 251 | auto entry = objmoves[index.row()]; 252 | objmoveStr = QString("push = %1, objhspd = %2, objang = %3") 253 | .arg(std::get<0>(entry) ? "yes" : "no") 254 | .arg(hypotf(std::get<1>(entry), std::get<2>(entry))) 255 | .arg(atan2f(std::get<2>(entry), std::get<1>(entry)) 256 | * M_RAD2DEG); 257 | } 258 | if (!basevelStr.isNull() && objmoveStr.isNull()) 259 | return basevelStr; 260 | else if (basevelStr.isNull() && !objmoveStr.isNull()) 261 | return objmoveStr; 262 | else if (!basevelStr.isNull() && !objmoveStr.isNull()) 263 | return basevelStr + QString(" | ") + objmoveStr; 264 | break; 265 | case HEAD_VSPD: 266 | if (vbasevels.contains(index.row())) 267 | return QString("vertical basevel = %1") 268 | .arg(vbasevels[index.row()]); 269 | break; 270 | case HEAD_PITCH: 271 | if (punchangles.contains(index.row())) 272 | return QString("punchpitch = %1") 273 | .arg(punchangles[index.row()].first); 274 | break; 275 | case HEAD_YAW: 276 | if (punchangles.contains(index.row())) 277 | return QString("punchyaw = %1") 278 | .arg(punchangles[index.row()].second); 279 | break; 280 | case HEAD_HP: 281 | case HEAD_AP: 282 | if (damages.contains(index.row())) { 283 | auto entry = damages[index.row()]; 284 | QString blastDistStr; 285 | QStringList dmgStrList; 286 | for (int flag : DMG_STRING.uniqueKeys()) { 287 | if (entry.second & flag) 288 | dmgStrList.append(DMG_STRING[flag]); 289 | } 290 | if (dmgStrList.isEmpty()) 291 | dmgStrList.append(entry.second ? "other" : "generic"); 292 | else if (entry.second & (1 << 6)) 293 | blastDistStr = QString(" dist = %1") 294 | .arg(explddists[index.row()]); 295 | return QString("damage = %1 (%2)%3").arg(entry.first) 296 | .arg(dmgStrList.join(", ")).arg(blastDistStr); 297 | } 298 | break; 299 | } 300 | break; 301 | } 302 | 303 | return QVariant(); 304 | } 305 | 306 | QVariant LogTableModel::headerData(int section, Qt::Orientation orientation, 307 | int role) const 308 | { 309 | if (role == Qt::FontRole && orientation == Qt::Vertical) { 310 | if (extralines.contains(section)) 311 | return boldFont; 312 | else 313 | return italicFont; 314 | } 315 | 316 | if (role == Qt::DisplayRole) { 317 | if (orientation == Qt::Vertical) { 318 | if (extralines.contains(section)) 319 | return '*' + QString::number(frameNums[section]); 320 | else 321 | return frameNums[section]; 322 | } else { 323 | return HEAD_LABELS[section]; 324 | } 325 | } 326 | 327 | if (role == Qt::UserRole && orientation == Qt::Vertical && 328 | extralines.contains(section)) { 329 | return extralines[section].join('\n'); 330 | } 331 | 332 | return QVariant(); 333 | } 334 | 335 | bool LogTableModel::parseLogFile(const QString &logFileName) 336 | { 337 | #define STARTTOK(n) tok = strtok_r(lineptr + (n), " ", &saveptr); 338 | #define NEXTTOK tok = strtok_r(NULL, " ", &saveptr); 339 | 340 | FILE *logFile = std::fopen(logFileName.toUtf8().data(), "r"); 341 | if (!logFile) 342 | return false; 343 | 344 | float basevel[3] = {0, 0, 0}; 345 | int readState = 0; 346 | LogEntry logEntry; 347 | size_t n = 1024; 348 | char *lineptr = (char *)std::malloc(n); 349 | 350 | while (getline(&lineptr, &n, logFile) != -1) { 351 | char *saveptr; 352 | char *tok; 353 | 354 | switch (readState) { 355 | case 0: 356 | if (strncmp(lineptr, "prethink", 8) == 0) { 357 | STARTTOK(8); 358 | frameNums.append(std::atoi(tok)); 359 | logTableData.append(logEntry); 360 | NEXTTOK; 361 | logTableData.last().frate = 1 / std::atof(tok); 362 | readState = 1; 363 | continue; 364 | } else if (strncmp(lineptr, "dmg", 3) == 0) { 365 | STARTTOK(3); 366 | float dmg = std::atof(tok); 367 | NEXTTOK; 368 | unsigned long bits = std::strtoul(tok, NULL, 10); 369 | damages[frameNums.length() - 1] = qMakePair(dmg, bits); 370 | continue; 371 | } else if (strncmp(lineptr, "obj", 3) == 0) { 372 | STARTTOK(3); 373 | bool push = *tok != '0'; 374 | NEXTTOK; 375 | float velx = std::atof(tok); 376 | NEXTTOK; 377 | float vely = std::atof(tok); 378 | objmoves[frameNums.length() - 1] = 379 | std::make_tuple(push, velx, vely); 380 | continue; 381 | } else if (strncmp(lineptr, "expld", 5) == 0) { 382 | float start[3]; 383 | STARTTOK(5); 384 | start[0] = std::atof(tok); 385 | NEXTTOK; 386 | start[1] = std::atof(tok); 387 | NEXTTOK; 388 | start[2] = std::atof(tok); 389 | 390 | float end[3]; 391 | NEXTTOK; 392 | NEXTTOK; 393 | NEXTTOK; 394 | NEXTTOK; 395 | end[0] = std::atof(tok); 396 | NEXTTOK; 397 | end[1] = std::atof(tok); 398 | NEXTTOK; 399 | end[2] = std::atof(tok); 400 | 401 | float disp[3] = {end[0] - start[0], end[1] - start[1], 402 | end[2] - start[2]}; 403 | explddists[frameNums.length() - 1] = sqrt( 404 | disp[0] * disp[0] + disp[1] * disp[1] + disp[2] * disp[2]); 405 | continue; 406 | } 407 | break; 408 | case 1: 409 | if (strncmp(lineptr, "health", 6) != 0) 410 | break; 411 | STARTTOK(6); 412 | logTableData.last().hp = std::atof(tok); 413 | NEXTTOK; 414 | logTableData.last().ap = std::atof(tok); 415 | readState = 2; 416 | continue; 417 | case 2: 418 | if (strncmp(lineptr, "usercmd", 7) != 0) 419 | break; 420 | STARTTOK(7); 421 | logTableData.last().msec = std::atoi(tok); 422 | NEXTTOK; 423 | logTableData.last().buttons = std::strtoul(tok, NULL, 10); 424 | NEXTTOK; 425 | logTableData.last().pitch = std::atof(tok); 426 | NEXTTOK; 427 | logTableData.last().yaw = std::atof(tok); 428 | readState = 3; 429 | continue; 430 | case 3: 431 | if (strncmp(lineptr, "fsu", 3) != 0) 432 | break; 433 | STARTTOK(3); 434 | logTableData.last().fmove = std::atoi(tok); 435 | NEXTTOK; 436 | logTableData.last().smove = std::atoi(tok); 437 | NEXTTOK; 438 | logTableData.last().umove = std::atoi(tok); 439 | readState = 4; 440 | continue; 441 | case 4: 442 | if (strncmp(lineptr, "fg", 2) != 0) 443 | break; 444 | readState = 5; 445 | continue; 446 | case 5: { 447 | if (strncmp(lineptr, "pa", 2) != 0) 448 | break; 449 | float tmppangs[2]; 450 | STARTTOK(2); 451 | tmppangs[0] = std::atof(tok); 452 | NEXTTOK; 453 | tmppangs[1] = std::atof(tok); 454 | if (tmppangs[0] || tmppangs[1]) 455 | punchangles[frameNums.length() - 1] = 456 | qMakePair(tmppangs[0], tmppangs[1]); 457 | readState = 6; 458 | continue; 459 | } 460 | case 6: 461 | if (strncmp(lineptr, "pmove", 5) != 0) 462 | break; 463 | STARTTOK(5); 464 | if (*tok != '1') 465 | break; 466 | NEXTTOK; 467 | NEXTTOK; 468 | NEXTTOK; 469 | NEXTTOK; 470 | basevel[0] = std::atof(tok); 471 | NEXTTOK; 472 | basevel[1] = std::atof(tok); 473 | NEXTTOK; 474 | basevel[2] = std::atof(tok); 475 | if (basevel[2]) 476 | vbasevels[frameNums.length() - 1] = basevel[2]; 477 | readState = 7; 478 | continue; 479 | case 7: 480 | if (strncmp(lineptr, "ntl", 3) != 0) 481 | break; 482 | STARTTOK(3); 483 | if (*tok != '0') 484 | numtouches.insert(frameNums.length() - 1); 485 | NEXTTOK; 486 | logTableData.last().ladder = *tok != '0'; 487 | readState = 8; 488 | continue; 489 | case 8: 490 | if (strncmp(lineptr, "pos", 3) != 0) 491 | break; 492 | STARTTOK(3); 493 | if (*tok != '2') 494 | break; 495 | NEXTTOK; 496 | logTableData.last().posx = std::atof(tok); 497 | NEXTTOK; 498 | logTableData.last().posy = std::atof(tok); 499 | NEXTTOK; 500 | logTableData.last().posz = std::atof(tok); 501 | readState = 9; 502 | continue; 503 | case 9: { 504 | if (strncmp(lineptr, "pmove", 5) != 0) 505 | break; 506 | STARTTOK(5); 507 | if (*tok != '2') 508 | break; 509 | 510 | float vel[3]; 511 | NEXTTOK; 512 | vel[0] = std::atof(tok); 513 | NEXTTOK; 514 | vel[1] = std::atof(tok); 515 | NEXTTOK; 516 | vel[2] = std::atof(tok); 517 | logTableData.last().hspd = hypotf(vel[0], vel[1]); 518 | logTableData.last().ang = atan2f(vel[1], vel[0]) * 519 | M_RAD2DEG; 520 | logTableData.last().vspd = vel[2]; 521 | 522 | NEXTTOK; 523 | NEXTTOK; 524 | NEXTTOK; 525 | NEXTTOK; 526 | bool bInDuck = *tok != '0'; 527 | NEXTTOK; 528 | bool ducking = std::strtoul(tok, NULL, 10) & FL_DUCKING; 529 | if (ducking) 530 | logTableData.last().dst = 2; 531 | else if (bInDuck) 532 | logTableData.last().dst = 1; 533 | else 534 | logTableData.last().dst = 0; 535 | 536 | NEXTTOK; 537 | logTableData.last().og = std::atoi(tok) != -1; 538 | NEXTTOK; 539 | logTableData.last().wlvl = std::atoi(tok); 540 | if (basevel[0] || basevel[1]) { 541 | vel[0] += basevel[0]; 542 | vel[1] += basevel[1]; 543 | hbasevels[frameNums.length() - 1] = qMakePair( 544 | hypotf(vel[0], vel[1]), 545 | atan2f(vel[1], vel[0]) * M_RAD2DEG); 546 | } 547 | readState = 0; 548 | continue; 549 | } 550 | } 551 | 552 | if (strncmp(lineptr, "pos", 3) == 0 || 553 | strncmp(lineptr, "cl_yawspeed", 11) == 0 || 554 | strncmp(lineptr, "execing", 7) == 0) 555 | continue; 556 | 557 | extralines[frameNums.length() - 1].append(lineptr); 558 | } 559 | 560 | std::free(lineptr); 561 | std::fclose(logFile); 562 | 563 | if (extralines.contains(-1)){ 564 | extralines[-1].append(extralines.value(0, QStringList())); 565 | extralines[0].swap(extralines[-1]); 566 | extralines.remove(-1); 567 | } 568 | 569 | beginInsertRows(QModelIndex(), 0, frameNums.length() - 1); 570 | endInsertRows(); 571 | 572 | return true; 573 | } 574 | 575 | void LogTableModel::clearAllRows() 576 | { 577 | beginRemoveRows(QModelIndex(), 0, frameNums.length() - 1); 578 | logTableData.clear(); 579 | frameNums.clear(); 580 | damages.clear(); 581 | punchangles.clear(); 582 | hbasevels.clear(); 583 | vbasevels.clear(); 584 | objmoves.clear(); 585 | numtouches.clear(); 586 | extralines.clear(); 587 | endRemoveRows(); 588 | } 589 | 590 | QModelIndex LogTableModel::findDiff(const QModelIndex &curIndex, 591 | bool forward) const 592 | { 593 | #define SEARCHDIFF(field) \ 594 | { \ 595 | auto curVal = logTableData[curIndex.row()].field; \ 596 | if (forward) { \ 597 | for (int row = curIndex.row() + 1; \ 598 | row < logTableData.length(); row++) { \ 599 | if ((logTableData[row].field) != curVal) \ 600 | return createIndex(row, curIndex.column()); \ 601 | } \ 602 | } else { \ 603 | for (int row = curIndex.row() - 1; row >= 0; row--) { \ 604 | if ((logTableData[row].field) != curVal) \ 605 | return createIndex(row, curIndex.column()); \ 606 | } \ 607 | } \ 608 | } 609 | 610 | if (!curIndex.isValid()) 611 | return QModelIndex(); 612 | 613 | switch (curIndex.column()) { 614 | case HEAD_FRATE: SEARCHDIFF(frate); break; 615 | case HEAD_MSEC: SEARCHDIFF(msec); break; 616 | case HEAD_HP: SEARCHDIFF(hp); break; 617 | case HEAD_AP: SEARCHDIFF(ap); break; 618 | case HEAD_HSPD: SEARCHDIFF(hspd); break; 619 | case HEAD_ANG: SEARCHDIFF(ang); break; 620 | case HEAD_VSPD: SEARCHDIFF(vspd); break; 621 | case HEAD_OG: SEARCHDIFF(og); break; 622 | case HEAD_DST: SEARCHDIFF(dst); break; 623 | case HEAD_DUCK: SEARCHDIFF(buttons & IN_DUCK); break; 624 | case HEAD_JUMP: SEARCHDIFF(buttons & IN_JUMP); break; 625 | case HEAD_FMOVE: SEARCHDIFF(fmove); break; 626 | case HEAD_SMOVE: SEARCHDIFF(smove); break; 627 | case HEAD_UMOVE: SEARCHDIFF(umove); break; 628 | case HEAD_YAW: SEARCHDIFF(yaw); break; 629 | case HEAD_PITCH: SEARCHDIFF(pitch); break; 630 | case HEAD_USE: SEARCHDIFF(buttons & IN_USE); break; 631 | case HEAD_ATTACK: SEARCHDIFF(buttons & IN_ATTACK); break; 632 | case HEAD_ATTACK2: SEARCHDIFF(buttons & IN_ATTACK2); break; 633 | case HEAD_RELOAD: SEARCHDIFF(buttons & IN_RELOAD); break; 634 | case HEAD_WLVL: SEARCHDIFF(wlvl); break; 635 | case HEAD_LADDER: SEARCHDIFF(ladder); break; 636 | case HEAD_POSX: SEARCHDIFF(posx); break; 637 | case HEAD_POSY: SEARCHDIFF(posy); break; 638 | case HEAD_POSZ: SEARCHDIFF(posz); break; 639 | } 640 | 641 | return QModelIndex(); 642 | } 643 | 644 | float LogTableModel::sumDuration(int startRow, int endRow) const 645 | { 646 | double duration = 0; 647 | for (int i = startRow; i <= endRow; i++) { 648 | duration += 1 / (double)logTableData[i].frate; 649 | } 650 | return duration; 651 | } 652 | -------------------------------------------------------------------------------- /utils/qconread/logtablemodel.h: -------------------------------------------------------------------------------- 1 | #ifndef LOGTABLEMODEL_H 2 | #define LOGTABLEMODEL_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | struct LogEntry 10 | { 11 | unsigned int buttons; 12 | float frate; 13 | float hp; 14 | float ap; 15 | float hspd; 16 | float ang; 17 | float vspd; 18 | float yaw; 19 | float pitch; 20 | float posx; 21 | float posy; 22 | float posz; 23 | short fmove; 24 | short smove; 25 | short umove; 26 | char msec; 27 | char og; 28 | char dst; 29 | char wlvl; 30 | char ladder; 31 | }; 32 | 33 | class LogTableModel : public QAbstractTableModel 34 | { 35 | public: 36 | // IMPORTANT: Make sure the labels in HEAD_LABELS matches that of 37 | // HeaderIndex. If you modify HeaderIndex, remember to modify HEAD_LABELS 38 | // and vice versa! 39 | enum HeaderIndex 40 | { 41 | HEAD_FRATE, 42 | HEAD_MSEC, 43 | HEAD_HP, 44 | HEAD_AP, 45 | HEAD_HSPD, 46 | HEAD_ANG, 47 | HEAD_VSPD, 48 | HEAD_OG, 49 | HEAD_DST, 50 | HEAD_DUCK, 51 | HEAD_JUMP, 52 | HEAD_FMOVE, 53 | HEAD_SMOVE, 54 | HEAD_UMOVE, 55 | HEAD_YAW, 56 | HEAD_PITCH, 57 | HEAD_USE, 58 | HEAD_ATTACK, 59 | HEAD_ATTACK2, 60 | HEAD_RELOAD, 61 | HEAD_WLVL, 62 | HEAD_LADDER, 63 | HEAD_POSX, 64 | HEAD_POSY, 65 | HEAD_POSZ, 66 | 67 | // This stays at the end 68 | HEAD_LENGTH, 69 | }; 70 | const QString HEAD_LABELS[HEAD_LENGTH] = { 71 | "fr", "ms", "hp", "ap", "hspd", "ang", "vspd", "og", "ds", "d", "j", 72 | "fm", "sm", "um", "yaw", "pitch", "u", "a", "a2", "rl", "wl", "ld", 73 | "px", "py", "pz" 74 | }; 75 | 76 | LogTableModel(QObject *parent); 77 | int columnCount(const QModelIndex &parent) const; 78 | int rowCount(const QModelIndex &parent) const; 79 | QVariant data(const QModelIndex &index, int role) const; 80 | QVariant headerData(int section, Qt::Orientation orientation, 81 | int role) const; 82 | bool parseLogFile(const QString &logFileName); 83 | void clearAllRows(); 84 | QModelIndex findDiff(const QModelIndex &curIndex, bool forward) const; 85 | float sumDuration(int startRow, int endRow) const; 86 | 87 | private: 88 | QVector logTableData; 89 | QVector frameNums; 90 | QHash> damages; 91 | QHash> punchangles; 92 | QHash> hbasevels; 93 | QHash vbasevels; 94 | QHash> objmoves; 95 | QHash extralines; 96 | QHash explddists; 97 | QSet numtouches; 98 | QFont italicFont; 99 | QFont boldFont; 100 | }; 101 | 102 | #endif 103 | -------------------------------------------------------------------------------- /utils/qconread/logtableview.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "logtableview.h" 3 | #include "logtablemodel.h" 4 | 5 | LogTableView::LogTableView(QWidget *parent) 6 | : QTableView(parent) 7 | { 8 | setCornerButtonEnabled(false); 9 | setMouseTracking(true); 10 | 11 | QHeaderView *horizHead = horizontalHeader(); 12 | horizHead->setSectionsMovable(true); 13 | horizHead->setSectionsClickable(false); 14 | 15 | QHeaderView *vertHead = verticalHeader(); 16 | vertHead->setDefaultSectionSize(vertHead->minimumSectionSize()); 17 | vertHead->setSectionResizeMode(QHeaderView::Fixed); 18 | } 19 | 20 | void LogTableView::setModel(LogTableModel *model) 21 | { 22 | QTableView::setModel(model); 23 | QHeaderView *horizHead = horizontalHeader(); 24 | int hMinSize = horizHead->minimumSectionSize(); 25 | for (int i : {LogTableModel::HEAD_MSEC, LogTableModel::HEAD_OG, 26 | LogTableModel::HEAD_DST, LogTableModel::HEAD_DUCK, 27 | LogTableModel::HEAD_JUMP, LogTableModel::HEAD_FMOVE, 28 | LogTableModel::HEAD_SMOVE, LogTableModel::HEAD_UMOVE, 29 | LogTableModel::HEAD_USE, LogTableModel::HEAD_ATTACK, 30 | LogTableModel::HEAD_ATTACK2, LogTableModel::HEAD_RELOAD, 31 | LogTableModel::HEAD_WLVL, LogTableModel::HEAD_LADDER}) { 32 | horizHead->resizeSection(i, hMinSize); 33 | horizHead->setSectionResizeMode(i, QHeaderView::Fixed); 34 | } 35 | for (int i : {LogTableModel::HEAD_HP, LogTableModel::HEAD_AP}) { 36 | horizHead->resizeSection(i, hMinSize * 2); 37 | } 38 | for (int i : {LogTableModel::HEAD_FRATE, LogTableModel::HEAD_YAW, 39 | LogTableModel::HEAD_PITCH, LogTableModel::HEAD_POSX, 40 | LogTableModel::HEAD_POSY, LogTableModel::HEAD_POSZ}) { 41 | horizHead->resizeSection(i, hMinSize * 3); 42 | } 43 | } 44 | 45 | LogTableModel *LogTableView::model() const 46 | { 47 | return (LogTableModel *)QTableView::model(); 48 | } 49 | 50 | void LogTableView::setIndexToDiff(bool forward) 51 | { 52 | QModelIndex newIndex = model()->findDiff(currentIndex(), forward); 53 | if (newIndex.isValid()) 54 | setCurrentIndex(newIndex); 55 | } 56 | 57 | void LogTableView::selectionChanged(const QItemSelection &selected, 58 | const QItemSelection &deselected) 59 | { 60 | QTableView::selectionChanged(selected, deselected); 61 | 62 | QModelIndexList indexList = selectedIndexes(); 63 | if (indexList.empty()) { 64 | emit(showNumFrames(0, 0)); 65 | return; 66 | } 67 | 68 | int min = indexList[0].row(); 69 | int max = indexList[0].row(); 70 | for (int i = 1; i < indexList.size(); i++) { 71 | if (indexList[i].row() < min) 72 | min = indexList[i].row(); 73 | else if (indexList[i].row() > max) 74 | max = indexList[i].row(); 75 | } 76 | 77 | emit(showNumFrames(max - min + 1, model()->sumDuration(min, max))); 78 | } 79 | -------------------------------------------------------------------------------- /utils/qconread/logtableview.h: -------------------------------------------------------------------------------- 1 | #ifndef LOGTABLEVIEW_H 2 | #define LOGTABLEVIEW_H 3 | 4 | #include 5 | #include "logtablemodel.h" 6 | 7 | class LogTableView : public QTableView 8 | { 9 | Q_OBJECT 10 | 11 | public: 12 | LogTableView(QWidget *parent); 13 | void setModel(LogTableModel *model); 14 | LogTableModel *model() const; 15 | void setIndexToDiff(bool forward); 16 | 17 | signals: 18 | void showNumFrames(int, float); 19 | 20 | protected: 21 | void selectionChanged(const QItemSelection &selected, 22 | const QItemSelection &deselected); 23 | }; 24 | 25 | #endif 26 | -------------------------------------------------------------------------------- /utils/qconread/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "qcreadwin.h" 3 | 4 | int main(int argc, char **argv) 5 | { 6 | QApplication app(argc, argv); 7 | QCReadWin qcreadWin; 8 | qcreadWin.show(); 9 | return app.exec(); 10 | } 11 | -------------------------------------------------------------------------------- /utils/qconread/qconread.pro: -------------------------------------------------------------------------------- 1 | ###################################################################### 2 | # Automatically generated by qmake (3.0) Sun Apr 27 12:43:11 2014 3 | ###################################################################### 4 | 5 | TEMPLATE = app 6 | TARGET = qconread 7 | INCLUDEPATH += . 8 | CONFIG += c++11 9 | QT += widgets 10 | 11 | # Input 12 | HEADERS += qcreadwin.h logtableview.h logtablemodel.h 13 | SOURCES += qcreadwin.cpp logtableview.cpp logtablemodel.cpp main.cpp 14 | -------------------------------------------------------------------------------- /utils/qconread/qcreadwin.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include "qcreadwin.h" 7 | #include "logtablemodel.h" 8 | 9 | static const QString WIN_NAME = "Qconsole Reader"; 10 | 11 | QCReadWin::QCReadWin() 12 | { 13 | resize(1000, 600); 14 | setWindowTitle(WIN_NAME); 15 | 16 | logTableView = new LogTableView(this); 17 | logTableView->setModel(new LogTableModel(logTableView)); 18 | connect(logTableView->verticalHeader(), SIGNAL(sectionClicked(int)), this, 19 | SLOT(showExtraLines(int))); 20 | connect(logTableView, SIGNAL(showNumFrames(int, float)), this, 21 | SLOT(showNumFrames(int, float))); 22 | setCentralWidget(logTableView); 23 | 24 | extraLinesDock = new QDockWidget("Extra lines", this); 25 | extraLinesDock->setFloating(false); 26 | extraLinesDock->hide(); 27 | addDockWidget(Qt::BottomDockWidgetArea, extraLinesDock); 28 | 29 | extraLinesEdit = new QPlainTextEdit(extraLinesDock); 30 | extraLinesEdit->setReadOnly(true); 31 | extraLinesDock->setWidget(extraLinesEdit); 32 | 33 | QMenu *menuFile = menuBar()->addMenu("&File"); 34 | menuFile->addAction("&Open log...", this, SLOT(openLogFile()), 35 | QKeySequence("Ctrl+O")); 36 | menuFile->addAction("&Reload log", this, SLOT(reloadLogFile()), 37 | QKeySequence("Ctrl+R")); 38 | menuFile->addAction("&Quit", this, SLOT(close()), QKeySequence("Ctrl+Q")); 39 | 40 | QMenu *menuView = menuBar()->addMenu("&View"); 41 | menuView->addAction(extraLinesDock->toggleViewAction()); 42 | 43 | QMenu *menuGo = menuBar()->addMenu("&Go"); 44 | menuGo->addAction("&Next different", this, SLOT(findNextDiff()), 45 | QKeySequence("]")); 46 | menuGo->addAction("&Prev different", this, SLOT(findPrevDiff()), 47 | QKeySequence("[")); 48 | 49 | QMenu *menuHelp = menuBar()->addMenu("&Help"); 50 | menuHelp->addAction("&About...", this, SLOT(showAbout())); 51 | 52 | lblNumFrames = new QLabel(statusBar()); 53 | statusBar()->addPermanentWidget(lblNumFrames); 54 | } 55 | 56 | void QCReadWin::findNextDiff() 57 | { 58 | logTableView->setIndexToDiff(true); 59 | } 60 | 61 | void QCReadWin::findPrevDiff() 62 | { 63 | logTableView->setIndexToDiff(false); 64 | } 65 | 66 | void QCReadWin::openLogFile() 67 | { 68 | logFileName = QFileDialog::getOpenFileName( 69 | this, "Open Log", "", "Log files (*.log);;All files (*.*)"); 70 | logTableView->setFocus(Qt::OtherFocusReason); 71 | reloadLogFile(); 72 | } 73 | 74 | void QCReadWin::reloadLogFile() 75 | { 76 | if (logFileName.isNull()) 77 | return; 78 | logTableView->model()->clearAllRows(); 79 | extraLinesEdit->clear(); 80 | if (!logTableView->model()->parseLogFile(logFileName)) 81 | QMessageBox::warning(this, "Error", "Failed to parse."); 82 | } 83 | 84 | void QCReadWin::showAbout() 85 | { 86 | QMessageBox::information(this, "About", R"(A basic reader for qconsole.log generated by TasTools mod. 87 | 88 | Written by Matherunner, 2014. You can modify the hell out of this application to add new functionalities.)"); 89 | } 90 | 91 | void QCReadWin::showExtraLines(int section) 92 | { 93 | QString extraLines = logTableView->model()->headerData( 94 | section, Qt::Vertical, Qt::UserRole).toString(); 95 | extraLinesEdit->setPlainText(extraLines); 96 | } 97 | 98 | void QCReadWin::showNumFrames(int numFrames, float duration) 99 | { 100 | if (numFrames < 2) 101 | lblNumFrames->setText(""); 102 | else 103 | lblNumFrames->setText(QString("no. of frames: %1, duration: %2") 104 | .arg(numFrames).arg(duration)); 105 | } 106 | -------------------------------------------------------------------------------- /utils/qconread/qcreadwin.h: -------------------------------------------------------------------------------- 1 | #ifndef QCREADWIN_H 2 | #define QCREADWIN_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include "logtableview.h" 9 | 10 | class QCReadWin : public QMainWindow 11 | { 12 | Q_OBJECT 13 | 14 | public: 15 | QCReadWin(); 16 | 17 | private slots: 18 | void findNextDiff(); 19 | void findPrevDiff(); 20 | void openLogFile(); 21 | void reloadLogFile(); 22 | void showAbout(); 23 | void showExtraLines(int); 24 | void showNumFrames(int, float); 25 | 26 | private: 27 | QString logFileName; 28 | LogTableView *logTableView; 29 | QDockWidget *extraLinesDock; 30 | QPlainTextEdit *extraLinesEdit; 31 | QLabel *lblNumFrames; 32 | }; 33 | 34 | #endif 35 | -------------------------------------------------------------------------------- /utils/taslaunch/gamecfg.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | from argparse import ArgumentParser 5 | 6 | parser = ArgumentParser() 7 | parser.add_argument('gamecfg', help='path to game.cfg') 8 | parser.add_argument('N1', type=int, help='number of waits before pause') 9 | parser.add_argument('N2', type=int, help='number of waits after pause') 10 | parser.add_argument('--save', help='save the game to SAVE at the end of game.cfg, usually used for saving during level transition') 11 | parser.add_argument('--trigger', action='store_true') 12 | args = parser.parse_args() 13 | 14 | if args.trigger: 15 | for line in sys.stdin: 16 | if line.startswith('GAME SKILL LEVEL'): 17 | break 18 | 19 | try: 20 | with open(args.gamecfg, 'w') as f: 21 | print('wait\n' * args.N1, end='', file=f) 22 | print('pause', file=f) 23 | print('wait\n' * args.N2, end='', file=f) 24 | if args.save is not None: 25 | print('save "{}"'.format(args.save), file=f) 26 | except OSError as e: 27 | print('Failed to write to game.cfg:', e, file=sys.stderr) 28 | sys.exit(1) 29 | -------------------------------------------------------------------------------- /utils/taslaunch/genlegit.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | from argparse import ArgumentParser 5 | from math import copysign 6 | 7 | parser = ArgumentParser() 8 | parser.add_argument('--prepend', metavar='CMDS', help='prepend CMDS to the output') 9 | parser.add_argument('--append', metavar='CMDS', help='append CMDS to the output') 10 | parser.add_argument('--hfrval', metavar='FRAMETIME', default='0.0001', help='value for the host_framerate before the final wait') 11 | parser.add_argument('--noendhfr', action='store_true', help='do not print a host_framerate before the final wait') 12 | parser.add_argument('--save', help='save the game to SAVE at the end of output') 13 | parser.add_argument('--record', metavar='DEMO', help='record the entire script to DEMO') 14 | args = parser.parse_args() 15 | 16 | AM_U_2 = 360 / 65536 / 2 17 | IN_ATTACK = 1 << 0 18 | IN_JUMP = 1 << 1 19 | IN_DUCK = 1 << 2 20 | IN_FORWARD = 1 << 3 21 | IN_BACK = 1 << 4 22 | IN_USE = 1 << 5 23 | IN_MOVELEFT = 1 << 9 24 | IN_MOVERIGHT = 1 << 10 25 | IN_ATTACK2 = 1 << 11 26 | IN_RELOAD = 1 << 13 27 | 28 | ftime = 0 29 | yawspeed_str = '' 30 | backspd_sign = '-' 31 | commands = [0] * 10 32 | pitch = None 33 | 34 | for line in sys.stdin: 35 | if line.rstrip() == 'CL_SignonReply: 2': 36 | break 37 | else: 38 | print('ERROR: Couldn\'t find "CL_SignonReply: 2"', file=sys.stderr) 39 | sys.exit(1) 40 | 41 | if args.prepend is not None: 42 | print(args.prepend) 43 | if args.record is not None: 44 | print('record', args.record) 45 | print('+left') 46 | print('cl_yawspeed 0') 47 | print('cl_forwardspeed 10000') 48 | print('cl_backspeed 10000') 49 | print('cl_sidespeed 10000') 50 | print('cl_upspeed 10000') 51 | 52 | for line in sys.stdin: 53 | if line.startswith('prethink'): 54 | new_ftime = float(line.rsplit(maxsplit=1)[1]) 55 | if not new_ftime: 56 | continue 57 | if new_ftime != ftime: 58 | print('host_framerate', new_ftime) 59 | ftime = new_ftime 60 | print('wait') 61 | print(yawspeed_str, end='') 62 | 63 | elif line.startswith('cl_yawspeed'): 64 | yawspeed_str = line 65 | 66 | elif line.startswith('weapon_'): 67 | print(line, end='') 68 | 69 | elif line.startswith('usercmd'): 70 | s = line.split() 71 | new_pitch = float(s[3]) 72 | if new_pitch != pitch: 73 | adjpitch = new_pitch + copysign(AM_U_2, new_pitch) 74 | print('cl_pitchup', -adjpitch) 75 | print('cl_pitchdown', adjpitch) 76 | pitch = new_pitch 77 | 78 | buttons = int(s[2]) 79 | for i, b, s in [(0, IN_FORWARD, 'forward'), 80 | (1, IN_MOVERIGHT, 'moveright'), 81 | (2, IN_MOVELEFT, 'moveleft'), 82 | (3, IN_BACK, 'back'), 83 | (4, IN_USE, 'use'), 84 | (5, IN_ATTACK, 'attack'), 85 | (6, IN_ATTACK2, 'attack2'), 86 | (7, IN_RELOAD, 'reload'), 87 | (8, IN_DUCK, 'duck'), 88 | (9, IN_JUMP, 'jump')]: 89 | newv = buttons & b 90 | if newv == commands[i]: 91 | continue 92 | print(('+' if newv else '-') + s) 93 | commands[i] = newv 94 | 95 | elif commands[3] and line.startswith('fsu'): 96 | s = line.split(maxsplit=2) 97 | new_backspd_sign = s[1][0] 98 | if new_backspd_sign != backspd_sign: 99 | print('cl_backspeed', '10000' if new_backspd_sign == '-' else '-10000') 100 | backspd_sign = new_backspd_sign 101 | 102 | if not args.noendhfr: 103 | print('host_framerate', args.hfrval) 104 | print('wait') 105 | print('-use') 106 | print('-attack') 107 | print('-attack2') 108 | print('-reload') 109 | print('-jump') 110 | print('-duck') 111 | print('-left') 112 | print('-forward') 113 | print('-moveleft') 114 | print('-moveright') 115 | print('-back') 116 | if args.record is not None: 117 | print('stop') 118 | if args.save is not None: 119 | print('save', args.save) 120 | print('echo TASEND') 121 | if args.append is not None: 122 | print(args.append) 123 | -------------------------------------------------------------------------------- /utils/taslaunch/gensim.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | 5 | for line in sys.stdin: 6 | line = line.strip() 7 | if line.startswith('//') or line.startswith('#') or not line: 8 | continue 9 | 10 | tokens = line.split() 11 | if tokens[0] == '@U': 12 | try: 13 | nwait = int(tokens[1]) 14 | niter = int(tokens[2]) 15 | usewait = 1 16 | if len(tokens) >= 4: 17 | usewait = int(tokens[3]) 18 | for _ in range(niter): 19 | print('wait\n' * nwait, end='') 20 | print('+use') 21 | print('wait\n' * usewait, end='') 22 | print('-use') 23 | except ValueError: 24 | print('Wrong argument type to @U', file=sys.stderr) 25 | sys.exit(1) 26 | except IndexError: 27 | print('@U needs two or three arguments', file=sys.stderr) 28 | sys.exit(1) 29 | continue 30 | 31 | try: 32 | evalstack = [] 33 | for token in tokens: 34 | if token == '+': 35 | evalstack[-2:] = [evalstack[-2] + evalstack[-1]] 36 | elif token == '-': 37 | evalstack[-2:] = [evalstack[-2] - evalstack[-1]] 38 | else: 39 | evalstack.append(int(token)) 40 | except (ValueError, IndexError): 41 | print(line) 42 | continue 43 | 44 | if len(evalstack) != 1: 45 | print('Wrong number of operators:', line, file=sys.stderr) 46 | sys.exit(1) 47 | 48 | if evalstack[0] < 1: 49 | print('Expression evaluates to < 1:', line, file=sys.stderr) 50 | sys.exit(1) 51 | 52 | print('wait\n' * evalstack[0], end='') 53 | -------------------------------------------------------------------------------- /utils/taslaunch/splitscript.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | from argparse import ArgumentParser 5 | from os.path import basename 6 | 7 | parser = ArgumentParser() 8 | parser.add_argument('N', type=int, help='number of lines per file') 9 | parser.add_argument('prefix', help='output file prefix (numbers followed by .cfg will be appended to this prefix)') 10 | args = parser.parse_args() 11 | 12 | if args.N < 1: 13 | print('The number of lines must be >= 1.') 14 | sys.exit(1) 15 | 16 | nlines = 0 17 | filenum = 1 18 | outfile = open(args.prefix + '.cfg', 'w') 19 | for line in sys.stdin: 20 | print(line, end='', file=outfile) 21 | nlines += 1 22 | if nlines < args.N: 23 | continue 24 | newname = '{}{}.cfg'.format(args.prefix, filenum) 25 | filenum += 1 26 | print('exec "{}"'.format(basename(newname)), file=outfile) 27 | outfile.close() 28 | outfile = open(newname, 'w') 29 | nlines = 0 30 | outfile.close() 31 | -------------------------------------------------------------------------------- /utils/taslaunch/taslaunch.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | import os 5 | import argparse 6 | import shlex 7 | import subprocess 8 | import shutil 9 | import configparser 10 | 11 | def print_error(line): 12 | print('ERROR:', line, file=sys.stderr) 13 | sys.exit(1) 14 | 15 | parser = argparse.ArgumentParser() 16 | parser.add_argument('--config', default='taslaunch.ini', help='path to taslaunch.ini') 17 | parser.add_argument('action', choices=['sim', 'legit'], help='action to perform') 18 | parser.add_argument('segment', help='name of segment to run') 19 | args = parser.parse_args() 20 | 21 | config = configparser.ConfigParser() 22 | config.read(args.config) 23 | 24 | config_section = None 25 | try: 26 | config_section = config[args.segment] 27 | except KeyError: 28 | print_error('Segment "{}" not found'.format(args.segment)) 29 | 30 | config_section['seg_name'] = args.segment 31 | dest_prefix = config_section.get(args.action + '_dest_prefix', 'tscript') 32 | sim_log = config_section.get('sim_log', args.segment + '_sim.log') 33 | 34 | waitpads = None 35 | waitpads_key = args.action + '_waitpads' 36 | try: 37 | N1, N2 = config_section[waitpads_key].split() 38 | waitpads = (int(N1), int(N2)) 39 | except KeyError: 40 | print_error(waitpads_key + ' not defined in the config file') 41 | except ValueError: 42 | print_error(waitpads_key + ' must be two integers') 43 | 44 | lines_per_file = None 45 | try: 46 | lines_per_file = config_section.getint('lines_per_file', 700) 47 | if lines_per_file < 1: 48 | raise ValueError 49 | except ValueError: 50 | print_error('lines_per_file must be an integer >= 1') 51 | 52 | load_cmd = '' 53 | load_from = '' 54 | try: 55 | load_cmd, load_from = config_section['load_from'].split() 56 | if load_cmd != 'map' and load_cmd != 'load': 57 | raise ValueError 58 | load_cmd = '+' + load_cmd 59 | except ValueError: 60 | print_error('load_from must be either "map " or "load "') 61 | except KeyError: 62 | load_cmd = '+load' 63 | load_from = args.segment 64 | 65 | hl_path = '' 66 | try: 67 | hl_path = os.environ['HL_PATH'] 68 | except KeyError: 69 | print_error('$HL_PATH not set') 70 | 71 | qcon_path = os.path.join(hl_path, 'qconsole.log') 72 | gamecfg_path = os.path.join(hl_path, 'valve', 'game.cfg') 73 | 74 | host_framerate = None 75 | try: 76 | host_framerate = config_section.getfloat('host_framerate', 0.0001) 77 | except ValueError: 78 | print_error('host_framerate must be a float') 79 | 80 | print('Removing qconsole.log...') 81 | try: 82 | os.remove(qcon_path) 83 | except OSError as e: 84 | pass 85 | 86 | print('Generating game.cfg...') 87 | ret = subprocess.call(['gamecfg.py', gamecfg_path, str(waitpads[0]), 88 | str(waitpads[1])]) 89 | if ret: 90 | print_error('gamecfg.py returned nonzero') 91 | 92 | if args.action == 'sim': 93 | sim_src = config_section.get('sim_src_script', args.segment + '_sim.cfg') 94 | sim_mod = config_section.get('sim_mod', 'valve') 95 | dest_path = os.path.join(hl_path, sim_mod, dest_prefix) 96 | 97 | print('Generating simulation script...') 98 | try: 99 | with open(sim_src, 'r') as f: 100 | gensim = subprocess.Popen('gensim.py', stdin=f, 101 | stdout=subprocess.PIPE) 102 | splitscript = subprocess.Popen( 103 | ['splitscript.py', str(lines_per_file), dest_path], 104 | stdin=gensim.stdout) 105 | 106 | gensim.wait() 107 | if gensim.returncode: 108 | print_error('gensim.py returned nonzero') 109 | 110 | splitscript.wait() 111 | if splitscript.returncode: 112 | print_error('splitscript.py returned nonzero') 113 | except OSError as e: 114 | print_error('Failed to generate simulation script:' + str(e)) 115 | 116 | sim_hl_args = shlex.split(config_section.get('sim_hl_args', '')) 117 | 118 | print('Executing Half-Life...') 119 | try: 120 | ret = subprocess.call(['runhl.sh', '-game', sim_mod, '-condebug', 121 | '+host_framerate', str(host_framerate), 122 | '+sv_taslog 1', load_cmd, load_from] + 123 | sim_hl_args) 124 | if ret: 125 | print('Half-Life returned nonzero (ignored)', file=sys.stderr) 126 | except OSError as e: 127 | print_error('Failed to execute Half-Life:' + str(e)) 128 | 129 | print('Copying qconsole.log...') 130 | try: 131 | shutil.copyfile(qcon_path, sim_log) 132 | except OSError as e: 133 | print_error('Failed to copy qconsole.log:' + str(e)) 134 | 135 | elif args.action == 'legit': 136 | legit_mod = config_section.get('legit_mod', 'valve') 137 | dest_path = os.path.join(hl_path, legit_mod, dest_prefix) 138 | dont_gen_legit = config_section.get('dont_gen_legit', None) 139 | 140 | if dont_gen_legit is None: 141 | print('Generating legitimate script...') 142 | try: 143 | with open(sim_log, 'r') as f: 144 | genlegit_args = ['genlegit.py', '--hfr', str(host_framerate)] 145 | if 'legit_demo' in config_section: 146 | genlegit_args.append('--record') 147 | genlegit_args.append(config_section['legit_demo']) 148 | if 'legit_save' in config_section: 149 | genlegit_args.append('--save') 150 | genlegit_args.append(config_section['legit_save']) 151 | if 'legit_prepend' in config_section: 152 | genlegit_args.append('--prepend') 153 | genlegit_args.append(config_section['legit_prepend']) 154 | if 'legit_append' in config_section: 155 | genlegit_args.append('--append') 156 | genlegit_args.append(config_section['legit_append']) 157 | 158 | genlegit = subprocess.Popen(genlegit_args, stdin=f, 159 | stdout=subprocess.PIPE) 160 | splitscript = subprocess.Popen( 161 | ['splitscript.py', str(lines_per_file), dest_path], 162 | stdin=genlegit.stdout) 163 | 164 | genlegit.wait() 165 | if genlegit.returncode: 166 | print_error('genlegit.py returned nonzero') 167 | 168 | splitscript.wait() 169 | if splitscript.returncode: 170 | print_error('splitscript.py returned nonzero') 171 | except OSError as e: 172 | print_error('Failed to generate legitimate script:' + str(e)) 173 | 174 | lvlwaitpads = None 175 | try: 176 | N1, N2 = config_section['legit_lvl_waitpads'].split(maxsplit=1) 177 | lvlwaitpads = (int(N1), int(N2)) 178 | except ValueError: 179 | print_error('legit_lvl_waitpads must be two integers') 180 | except KeyError: 181 | pass 182 | 183 | lvlsave = config_section.get('legit_lvl_save', None) 184 | legit_hl_args = shlex.split(config_section.get('legit_hl_args', '')) 185 | 186 | print('Executing Half-Life...') 187 | hl_args = ['runhl.sh', '-game', legit_mod, '-condebug', 188 | '+host_framerate', str(host_framerate), 189 | load_cmd, load_from] + legit_hl_args 190 | if lvlwaitpads is not None or lvlsave is not None: 191 | try: 192 | hl_proc = subprocess.Popen(hl_args, stderr=subprocess.PIPE) 193 | 194 | gamecfg_args = ['gamecfg.py', '--trigger', gamecfg_path] 195 | if lvlwaitpads is not None: 196 | gamecfg_args.append(str(lvlwaitpads[0])) 197 | gamecfg_args.append(str(lvlwaitpads[1])) 198 | else: 199 | gamecfg_args.append(str(waitpads[0])) 200 | gamecfg_args.append(str(waitpads[1])) 201 | 202 | if lvlsave is not None: 203 | gamecfg_args.append('--save') 204 | gamecfg_args.append(lvlsave) 205 | 206 | gamecfg_proc = subprocess.Popen(gamecfg_args, stdin=hl_proc.stderr) 207 | 208 | gamecfg_proc.wait() 209 | if gamecfg_proc.returncode: 210 | print_error('gamecfg.py returned nonzero') 211 | 212 | hl_proc.stderr.close() # prevent freezing 213 | hl_proc.wait() 214 | if hl_proc.returncode: 215 | print('Half-Life returned nonzero (ignored)', file=sys.stderr) 216 | except OSError as e: 217 | print_error('Failed to execute Half-Life and/or gamecfg.py:' + 218 | str(e)) 219 | else: 220 | try: 221 | ret = subprocess.call(hl_args) 222 | if ret: 223 | print('Half-Life returned nonzero (ignored)', file=sys.stderr) 224 | except OSError as e: 225 | print_error('Failed to execute Half-Life:' + str(e)) 226 | --------------------------------------------------------------------------------