<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>andr&#39;s den</title>
    <link>https://blog.andr.host/</link>
    <description>andr&#39;s den</description>
    <generator>Hugo -- gohugo.io</generator>
    <language>en</language>
    <managingEditor>florczak.raf&#43;blog@gmail.com (Rafał Florczak)</managingEditor>
    <webMaster>florczak.raf&#43;blog@gmail.com (Rafał Florczak)</webMaster>
    <lastBuildDate>Wed, 12 Mar 2025 00:00:00 +0000</lastBuildDate>
    
    <atom:link href="https://blog.andr.host/index.xml" rel="self" type="application/rss+xml" />
    
    
    <item>
      <title>TIL: Dynamically Extending the CMAKE_ARGS for External Project</title>
      <link>https://blog.andr.host/posts/2025/03/12-cmake-external-project/</link>
      <pubDate>Wed, 12 Mar 2025 00:00:00 +0000</pubDate>
      <author>florczak.raf&#43;blog@gmail.com (Rafał Florczak)</author>
      <guid>https://blog.andr.host/posts/2025/03/12-cmake-external-project/</guid>
      <description>
        <![CDATA[I&rsquo;ve recently introduced a bug that prevented the cross-compilation of the project for x86_64 from ARM-based macOS. In this short piece, I&rsquo;d like to summarize what happened and how to fix it.
]]>
      </description>
      
        
          <category>TIL</category>
        
      
      <content:encoded><![CDATA[<p>I&rsquo;ve recently introduced a bug that prevented the cross-compilation of the project for x86_64 from ARM-based macOS.
In this short piece, I&rsquo;d like to summarize what happened and how to fix it.</p>
<p><a href="https://github.com/itgmania/itgmania">ITGmania</a> project is an effort to modernize the <a href="https://github.com/stepmania/stepmania">StepMania</a> game, a <a href="https://en.wikipedia.org/wiki/Dance_Dance_Revolution">Dance Dance Revolution</a> arcade game simulator.
It&rsquo;s a fork that receives regular updates, both security and new features.</p>
<p>In January <a href="https://github.com/itgmania/itgmania/pull/596">I proposed to replace</a> an old vendored <code>libjpeg</code> library with a git submodule<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup> of a popular drop-in replacement, <a href="https://github.com/libjpeg-turbo/libjpeg-turbo"><code>libjpeg-turbo</code></a>.
It uses SIMD instructions to accelerate compression and decompression operations, so we could also get some performance gains for free.
The crucial part of the whole story is that <code>libjpeg-turbo</code> uses <a href="https://cmake.org/">CMake</a> as its build system.</p>
<p>That&rsquo;s great news because ITGmania also uses it, so the old glue CMake module for building <code>libjpeg</code> could be replaced with an <a href="https://cmake.org/cmake/help/v3.31/module/ExternalProject.html#external-project-definition"><code>ExternalProject</code></a>.
I wrote a short module to specify the library, how to build it, and the target for linking in the main project.</p>
<p>After some tweaking related to the <a href="https://ninja-build.org/">Ninja generator</a> that&rsquo;s by default used on Windows in Visual Studio, all the obvious problems have been resolved and the automatic builds in the CI pipeline in GitHub Actions were green.
The game worked with the new library.</p>
<p>The PR was merged a week ago, so the &ldquo;real-world integration&rdquo; could begin ;)
First, there was an issue reported about linking the project on Arch Linux.
It turned out that the output lib directory was using a different scheme &ndash; <code>lib64</code> instead of <code>lib</code> that was expected by the linker.
I quickly fixed that and submitted <a href="https://github.com/itgmania/itgmania/pull/626">a PR</a>.</p>
<p>Yesterday, <a href="https://github.com/teejusb">teejusb</a>, the project maintainer, contacted me about linking issues on macOS.
He got an <code>Undefined symbols for architecture x86_64</code> message related to some symbols from the <code>libjpeg</code> library, so I assumed he was building on an x86_64 macOS.
That&rsquo;s weird because we perform that build in the pipeline on every commit and it passed.</p>
<p>I don&rsquo;t have a Mac, so I created <a href="https://github.com/foxlet/macOS-Simple-KVM">a fresh KVM VM with macOS</a> in order to reproduce the issue.
I had some problems with getting the GPU passthrough to work, but I managed to build and start the game on a virtual GPU.
It compiled and linked &ndash; as expected.</p>
<p>What he didn&rsquo;t tell me was that he was trying to cross-compile the x86_64 target from his Apple Silicon macOS (some aarch64).
I traced the issue to the two bugs in these 3 lines of the original PR:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-cmake" data-lang="cmake"><span style="display:flex;"><span>if(<span style="color:#e6db74">MACOSX</span>)<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span>  set(<span style="color:#e6db74">ARCH_FLAGS</span> <span style="color:#e6db74">&#34;-DCMAKE_OSX_DEPLOYMENT_TARGET=${CMAKE_OSX_DEPLOYMENT_TARGET} -DCMAKE_OSX_ARCHITECTURES=${CMAKE_OSX_ARCHITECTURES}&#34;</span>)<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span>endif()<span style="color:#960050;background-color:#1e0010">
</span></span></span></code></pre></div><p>The first one being the wrong variable to check for macOS &ndash; it should be <code>APPLE</code>, not <code>MACOSX</code>.
The other one was more intriguing, it was related to how <code>ARCH_FLAGS</code> were used later in the <code>ExternalProject_Add</code> call within <code>CMAKE_ARGS</code>.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-cmake" data-lang="cmake"><span style="display:flex;"><span>include(<span style="color:#e6db74">ExternalProject</span>)<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span>ExternalProject_Add(
</span></span><span style="display:flex;"><span>  <span style="color:#e6db74">libjpeg_turbo_project</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#e6db74">SOURCE_DIR</span> <span style="color:#f92672">${</span>SM_EXTERN_DIR<span style="color:#f92672">}</span><span style="color:#e6db74">/libjpeg-turbo</span>
</span></span><span style="display:flex;"><span>  <span style="color:#e6db74">INSTALL_DIR</span> <span style="color:#f92672">${</span>INSTALL_DIR<span style="color:#f92672">}</span>
</span></span><span style="display:flex;"><span>  <span style="color:#e6db74">CMAKE_ARGS</span> <span style="color:#e6db74">-DCMAKE_INSTALL_PREFIX=</span><span style="color:#f92672">${</span>INSTALL_DIR<span style="color:#f92672">}</span>
</span></span><span style="display:flex;"><span>             <span style="color:#e6db74">-DCMAKE_INSTALL_LIBDIR=</span><span style="color:#f92672">${</span>LIB_DIR<span style="color:#f92672">}</span>
</span></span><span style="display:flex;"><span>             <span style="color:#e6db74">-DENABLE_SHARED=OFF</span>
</span></span><span style="display:flex;"><span>             <span style="color:#e6db74">-DENABLE_STATIC=ON</span>
</span></span><span style="display:flex;"><span>             <span style="color:#f92672">${</span>ARCH_FLAGS<span style="color:#f92672">}</span>
</span></span><span style="display:flex;"><span>  (<span style="color:#e6db74">...</span>)
</span></span></code></pre></div><p>After changing <code>ARCH_FLAGS</code> to <a href="https://cmake.org/cmake/help/v3.31/command/list.html">a list</a>, I ended up with:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-cmake" data-lang="cmake"><span style="display:flex;"><span>if(<span style="color:#e6db74">APPLE</span>)<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span>  LIST(<span style="color:#e6db74">APPEND</span> <span style="color:#e6db74">ARCH_FLAGS</span>
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">-DCMAKE_OSX_DEPLOYMENT_TARGET=</span><span style="color:#f92672">${</span>CMAKE_OSX_DEPLOYMENT_TARGET<span style="color:#f92672">}</span>
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">-DCMAKE_OSX_ARCHITECTURES=</span><span style="color:#f92672">${</span>CMAKE_OSX_ARCHITECTURES<span style="color:#f92672">}</span>
</span></span><span style="display:flex;"><span>  )<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span>endif()<span style="color:#960050;background-color:#1e0010">
</span></span></span></code></pre></div><p>This now properly passes the target and architecture to the <code>libjpeg-turbo</code> build.
I&rsquo;ve submitted it as <a href="https://github.com/itgmania/itgmania/pull/644">a Pull Request</a> this morning.</p>
<p>The old code never executed the <code>if</code> branch in the first place, and even when I changed the condition to <code>APPLE</code>, the variable didn&rsquo;t work as intended.
So, why did it seem to work? Why was it merged?
It&rsquo;s simply because the builds in the pipeline never exercised the cross-compilation path &ndash; they used separate hardware for each macOS build.</p>
<p>I also started <a href="https://github.com/itgmania/itgmania/issues/645">an issue</a> to propose new automatic builds to cover more actual use cases, not just the basic ones.
They will hopefully prevent similar issues from happening in the future.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>Poor-man&rsquo;s dependency management :&lt; I don&rsquo;t like this, but that&rsquo;s how the maintainers of the project want the deps to be specified and fetched.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>]]></content:encoded>
    </item>
    
    <item>
      <title>Reverse Engineering For Fun And (Non)profit</title>
      <link>https://blog.andr.host/posts/2025/02/27-reverse-engineering-for-fun-and-non-profit/</link>
      <pubDate>Thu, 27 Feb 2025 00:00:00 +0000</pubDate>
      <author>florczak.raf&#43;blog@gmail.com (Rafał Florczak)</author>
      <guid>https://blog.andr.host/posts/2025/02/27-reverse-engineering-for-fun-and-non-profit/</guid>
      <description>
        <![CDATA[I really hate it when the documentation is missing, or worse, outdated and incomplete. Unfortunately, this is the case for the Behringer XR18 digital mixer.
]]>
      </description>
      
        
          <category>reverse-engineering</category>
        
      
      <content:encoded><![CDATA[<p>I really hate it when the documentation is missing, or worse, outdated and incomplete. Unfortunately, this is the case for the <a href="https://www.behringer.com/product.html?modelCode=0605-AAD">Behringer XR18 digital mixer</a>.</p>
<h2 id="background" >Background
<span>
    <a href="#background">
        <svg viewBox="0 0 28 23" height="100%" width="19" xmlns="http://www.w3.org/2000/svg"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" fill="none" stroke-linecap="round" stroke-miterlimit="10" stroke-width="2"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" fill="none" stroke-linecap="round" stroke-miterlimit="10" stroke-width="2"/></svg>
    </a>
</span>
</h2><p>Let&rsquo;s step back for a moment to introduce a little background.
I&rsquo;m a <a href="https://gsps.pl/fundacja">Tech volunteer at GSPS Foundation</a> which hosts the Polish bi-yearly (as-in happening twice a year) charity <a href="https://en.wikipedia.org/wiki/Speedrunning">speedrunning</a> marathon based on the formula popularized by <a href="https://gamesdonequick.com/">Games Done Quick</a> (GDQ).
GSPS stands for Gramy Szybko, Pomagamy Skutecznie, which roughly translates to &ldquo;Gaming Swiftly, Providing Support&rdquo;<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup> when we try to keep the acronym.</p>
<p>The next GSPS event will be <a href="https://gsps.pl/wydarzenia/gspsdzie2025">GSPS Dzieciom 2025</a>. It&rsquo;s scheduled for March 26-30. You can <a href="https://gsps.pl/sklep/produkt/bilet-gsps-dzieciom-2025-normalny/">buy a ticket</a> and visit us in Kraków in person or watch the live transmissions on <a href="https://twitch.tv/gramypomagamy">Twitch</a> and <a href="https://www.youtube.com/@GramySzybkoPomagamySkutecznie">YouTube</a>.</p>
<p>The Foundation owns a digital audio mixer, a Behringer XR18, which offers great mixing capabilities for its price.
We currently use all but one of the input channels during the events, so we&rsquo;re dangerously close to the limit.</p>
<p>GDQ has an interesting way of indicating who is currently speaking on the stream.</p>
<table>
  <thead>
      <tr>
          <th style="text-align: center"><img src="https://blog.andr.host/2025/02/mixer-gdq-input-indicators.png" alt="GDQ&rsquo;s audio input indicators" title="GDQ&#39;s audio input indicators."></th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: center">GDQ&rsquo;s audio input indicators.</td>
      </tr>
  </tbody>
</table>
<p>It mimics the classic UV meter bars that meet at the center of the speaker&rsquo;s label.
I wondered if it would be possible to implement something similar in <a href="https://github.com/gramyPomagamy/gsps-layouts/">our stream layouts</a> because I knew that they use an older brother of our mixer, an <a href="https://www.behringer.com/product.html?modelCode=0603-ACE">X32</a>.</p>
<h2 id="oh-theres-an-api" >Oh, There&rsquo;s An API
<span>
    <a href="#oh-theres-an-api">
        <svg viewBox="0 0 28 23" height="100%" width="19" xmlns="http://www.w3.org/2000/svg"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" fill="none" stroke-linecap="round" stroke-miterlimit="10" stroke-width="2"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" fill="none" stroke-linecap="round" stroke-miterlimit="10" stroke-width="2"/></svg>
    </a>
</span>
</h2><p>Behringer&rsquo;s X-AIR-Edit app is the official way to control the mixer.
It somehow manages to display the current signal input level for each channel in nearly real-time.
I just needed to know how, so I could use that information in our code.</p>
<table>
  <thead>
      <tr>
          <th style="text-align: center"><img src="https://blog.andr.host/2025/02/mixer-xr-input-indicators.png" alt="X-AIR-Edit&rsquo;s channel input indicators" title="X-AIR-Edit channel input indicators."></th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: center">X-AIR-Edit channel input indicators &ndash; the green bars next to the faders.</td>
      </tr>
  </tbody>
</table>
<p><a href="https://blog.onyxandiris.online/">Onyx</a>, an old friend of mine, has already researched the topic of the API offered by the whole family of Behringer&rsquo;s digital mixers and wrote <a href="https://github.com/onyx-and-iris/xair-api-python/">a python library for interacting with them</a>.
It turns out that they use a standard <a href="https://en.wikipedia.org/wiki/Open_Sound_Control">OSC</a> (Open Sound Control) protocol over UDP.</p>
<p>It&rsquo;s a simple protocol. Each message consists of an address and a types tag, followed by some values for the fields specified in the aforementioned tag.
The protocol requires 4-byte alignment using null bytes.
Luckily, there are libraries to handle the protocol, so we don&rsquo;t have to (yet ;).</p>
<p>I contacted Onyx because I couldn&rsquo;t find anything in his library that would allow me to query the input levels.
He directed me to <a href="https://cdn.mediavalet.com/aunsw/musictribe/hmivS7F05kKSJlWDYPGLpA/agTKkSIWS0y1bJl97YZEjg/Original/X%20AIR%20Remote%20Control%20Protocol.pdf">the official docs</a>, which are allegedly suitable for the firmware version &ldquo;1.11 or higher&rdquo; (our mixer runs on the newest, 1.22 firmware).
I found what I was looking for.
The <code>/meters</code> command was supposed to be an answer to all my needs.</p>
<h2 id="enter-the-docs" >Enter The Docs
<span>
    <a href="#enter-the-docs">
        <svg viewBox="0 0 28 23" height="100%" width="19" xmlns="http://www.w3.org/2000/svg"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" fill="none" stroke-linecap="round" stroke-miterlimit="10" stroke-width="2"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" fill="none" stroke-linecap="round" stroke-miterlimit="10" stroke-width="2"/></svg>
    </a>
</span>
</h2><p>The input levels that I was interested in are returned as a &ldquo;blob&rdquo; of bytes.
For example <code>/meters/2</code> yields an i32 (the size of the payload) and 36 i16s (16 analog, 2 aux, and 18 USB inputs) that we have to decode manually because the OSC protocol doesn&rsquo;t specify i16 type, so the OSC libraries will just return the raw bytes.
The i16 values&rsquo; resolution is specified to be 1/256 dB, that&rsquo;s pretty simple and more than enough for our needs.
To convert them to dB we just need to divide the raw value by 256.</p>
<p>So far so good, I quickly started hacking some PoC to connect to the mixer and request the <code>/meters/2</code>.
Everything worked as expected, I could read and decode the values and match them with what I was seeing in the official app.</p>
<table>
  <thead>
      <tr>
          <th style="text-align: center"><video controls autoplay muted loop>
    <source src="https://blog.andr.host/2025/02/mixer.mp4" type="video/mp4">
</video>
</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: center">PoC of the solution.</td>
      </tr>
  </tbody>
</table>
<p>I was happy that it worked.
However, that hasn&rsquo;t lasted very long.
Every time I started my script, it only worked for roughly 10 seconds before freezing.
The docs for <code>/meters/</code> clearly only stated that:</p>
<blockquote>
<p>Results in regular updates to batch of meter values as a single binary blob (&hellip;)</p>
</blockquote>
<h2 id="debugging" >Debugging
<span>
    <a href="#debugging">
        <svg viewBox="0 0 28 23" height="100%" width="19" xmlns="http://www.w3.org/2000/svg"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" fill="none" stroke-linecap="round" stroke-miterlimit="10" stroke-width="2"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" fill="none" stroke-linecap="round" stroke-miterlimit="10" stroke-width="2"/></svg>
    </a>
</span>
</h2><p>I checked that there were always 200 responses from the mixer every time I started the app and subscribed to the meters.
One every 50 ms; that gives the 10 seconds I observed.
Perhaps that&rsquo;s an intended behavior, not a bug.</p>
<p>Technically, I could just re-request the <code>/meters/2</code> every 200 messages or 10 seconds, but considering that this runs over UDP, there are no guarantees for deliveries or timings.</p>
<p>I decided to sniff the traffic of the official X-AIR-Edit app with Wireshark to see what they do.
I knew the mixer&rsquo;s address and that we&rsquo;re using UDP, so the filter was a pretty straightforward one, like: <code>udp &amp;&amp; ip.src == 192.168.11.167</code>.
There was <strong>a lot</strong> of traffic exchanged between the mixer and the app.</p>
<p>I further refined the filter to look just for the meters: <code>&amp;&amp; data contains &quot;meters/1&quot;</code> (they used different combinations of meters to display the same info; <code>/2</code> is what I ultimately needed, but that&rsquo;s not important here).
That&rsquo;s when I realized that every two seconds there&rsquo;s a periodic message from the app to the mixer, that looks like this:</p>
<table>
  <thead>
      <tr>
          <th style="text-align: center"><img src="https://blog.andr.host/2025/02/mixer-wireshark.png" alt="xr vs wireshark" title="xr vs wireshark"></th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: center">Periodic message sent from the X-AIR-Edit app to the mixer captured in Wireshark.</td>
      </tr>
  </tbody>
</table>
<p>Huh? What&rsquo;s that <code>/renew</code> OSC command?
I haven&rsquo;t seen anything about it in the docs.</p>
<h2 id="renew-the-subscription" >Renew The Subscription
<span>
    <a href="#renew-the-subscription">
        <svg viewBox="0 0 28 23" height="100%" width="19" xmlns="http://www.w3.org/2000/svg"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" fill="none" stroke-linecap="round" stroke-miterlimit="10" stroke-width="2"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" fill="none" stroke-linecap="round" stroke-miterlimit="10" stroke-width="2"/></svg>
    </a>
</span>
</h2><p>It turns out that there&rsquo;s an undocumented feature that refreshes the subscription for the meters.
That makes a lot of sense, as it lets the mixer stop sending the data in case of network issues or app failures, we&rsquo;re using UDP after all.</p>
<p>The syntax is pretty obvious (dots for null bytes):</p>
<pre tabindex="0"><code>/renew..,s..meters/1....
</code></pre><p><code>,s</code> means that there will be a single null-terminated string in the payload.</p>
<p>I quickly added a periodic <code>/renew</code> call to my code and observed that the mixer now sends me the updates without any interruptions.</p>
<h2 id="final-integration" >Final Integration
<span>
    <a href="#final-integration">
        <svg viewBox="0 0 28 23" height="100%" width="19" xmlns="http://www.w3.org/2000/svg"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" fill="none" stroke-linecap="round" stroke-miterlimit="10" stroke-width="2"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" fill="none" stroke-linecap="round" stroke-miterlimit="10" stroke-width="2"/></svg>
    </a>
</span>
</h2><p>GSPS stream layouts are based on the <a href="https://www.nodecg.dev/">NodeCG</a> framework that supports building interactive graphics and overlays for live broadcasts.
I wrapped everything together into a NodeCG extension and added some glue code for passing the state between the extension and the rendered overlays.
Here&rsquo;s a preview of the final effect:</p>
<table>
  <thead>
      <tr>
          <th style="text-align: center"><video controls autoplay muted loop>
    <source src="https://blog.andr.host/2025/02/mixer-integration.mp4" type="video/mp4">
</video>
</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: center">Preview of the final integration in the stream layouts.</td>
      </tr>
  </tbody>
</table>
<p>I didn&rsquo;t really like the aggressive animation on the GDQ layouts, so I opted for the subtle background highlights.</p>
<p>The implementation has been submitted to the public repository in this <a href="https://github.com/GramyPomagamy/gsps-layouts/pull/101">Pull Request</a>.</p>
<h2 id="closing-words" >Closing Words
<span>
    <a href="#closing-words">
        <svg viewBox="0 0 28 23" height="100%" width="19" xmlns="http://www.w3.org/2000/svg"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" fill="none" stroke-linecap="round" stroke-miterlimit="10" stroke-width="2"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" fill="none" stroke-linecap="round" stroke-miterlimit="10" stroke-width="2"/></svg>
    </a>
</span>
</h2><p>If you&rsquo;re offering a public API, please document it properly and keep it updated &ndash; it saves developers time and frustration.
The Wireshark part in this story could have been entirely avoided if the manufacturer updated the docs.</p>
<p>I&rsquo;m not entirely sure what&rsquo;s the case here &ndash; was the <code>/renew</code> not available in the <code>1.11</code> firmware?
Is it an omission and the docs will eventually be updated?
Was it a deliberate decision not to share that endpoint, hence rendering the entire <code>/meters</code> feature inconvenient to use?</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>Shout-out to myamai for chipping in the English GSPS acronym expansion.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>]]></content:encoded>
    </item>
    
    <item>
      <title>My RSS Now Includes Whole Posts</title>
      <link>https://blog.andr.host/posts/2024/11/05-rss-now-includes-whole-posts/</link>
      <pubDate>Wed, 06 Nov 2024 00:00:00 +0000</pubDate>
      <author>florczak.raf&#43;blog@gmail.com (Rafał Florczak)</author>
      <guid>https://blog.andr.host/posts/2024/11/05-rss-now-includes-whole-posts/</guid>
      <description>
        <![CDATA[I realized that my RSS reader displayed full articles for some blogs and not for others. That made me investigate the matter on my blog.
]]>
      </description>
      
        
          <category>misc</category>
        
      
      <content:encoded><![CDATA[<p>I realized that my RSS reader displayed full articles for some blogs and not for others.
That made me investigate the matter on my blog.</p>
<p>I usually prefer to read everything in one place myself.
I understand it&rsquo;s tempting for some portals to monetize through ads or deals when people visit them directly.
It also allows them to track traffic, user behavior, etc. to focus on creating content that sells better.</p>
<p>However, I don&rsquo;t have any ads or tracking, and I don&rsquo;t plan to introduce them in the foreseeable future.
And I&rsquo;m all in for letting people consume the content in the way that&rsquo;s most comfortable for them.
I believe that serving the whole posts over RSS also makes them more accessible for people with specific needs, who require bigger fonts, higher contrast, or text-to-speech because they can customize everything in one place.</p>
<p>My blog&rsquo;s RSS used to include only the <code>description</code> tag with the first paragraph, but now you can read everything if your reader supports <code>&lt;content:encoded&gt;</code> tag.
The blog is powered by <a href="https://gohugo.io/">Hugo</a> so it was only a matter of changing <a href="https://github.com/florczakraf/hugo-theme-anubis/commit/ef885ef4471addbb55b3e9e85aecb2cae45c5408">image rendering to use absolute URLs</a> and <a href="https://github.com/florczakraf/hugo-theme-anubis/commit/21fc368ddb70091bcd5b732cc2f830c492ac5228">including the <code>&lt;content:encoded&gt;</code> tag for each post</a> in the <a href="https://github.com/florczakraf/hugo-theme-anubis">theme</a>.</p>
<p>I also updated the theme to use <a href="https://github.com/florczakraf/hugo-theme-anubis/commit/9c9e10130b909d8622be6f97954601fccac43158">new Hugo configuration options instead of the deprecated ones</a> to silence the build warnings that were soon to be errors even though I don&rsquo;t use them. I found out that the standard authorship tags were also missing from the rendered XML, so I <a href="https://github.com/florczakraf/hugo-theme-anubis/commit/acf66d683a51eb69df0bbddb7d00d817af0752b8">updated them to match the variables defined in my site&rsquo;s config</a>.</p>]]></content:encoded>
    </item>
    
    <item>
      <title>Check Your Caches Regularly</title>
      <link>https://blog.andr.host/posts/2024/10/31-check-your-caches-regularly/</link>
      <pubDate>Thu, 31 Oct 2024 00:00:00 +0000</pubDate>
      <author>florczak.raf&#43;blog@gmail.com (Rafał Florczak)</author>
      <guid>https://blog.andr.host/posts/2024/10/31-check-your-caches-regularly/</guid>
      <description>
        <![CDATA[I ran out of disk space on my work laptop recently. Again. It finally motivated me to find the root cause.
]]>
      </description>
      
        
          <category>misc</category>
        
      
      <content:encoded><![CDATA[<p>I ran out of disk space on my work laptop recently. Again.
It finally motivated me to find the root cause.</p>
<h2 id="numbers" >Numbers
<span>
    <a href="#numbers">
        <svg viewBox="0 0 28 23" height="100%" width="19" xmlns="http://www.w3.org/2000/svg"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" fill="none" stroke-linecap="round" stroke-miterlimit="10" stroke-width="2"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" fill="none" stroke-linecap="round" stroke-miterlimit="10" stroke-width="2"/></svg>
    </a>
</span>
</h2><p>Let&rsquo;s talk numbers.
My main disk, a modest 500 GB SSD by today&rsquo;s standards, can fill up pretty quickly.</p>
<pre tabindex="0"><code>$ df -h /
Filesystem                  Size Used Avail Use% Mounted on
/dev/mapper/vgxubuntu-root  467G 467G     0 100% /
</code></pre><p>A lot of data has crossed this disk since the system installation at the beginning of 2021:</p>
<pre tabindex="0"><code># smartctl -a /dev/nvme0
(...)
Data Units Read:                    12,215,530 [6.25 TB]
Data Units Written:                 25,077,401 [12.8 TB]
</code></pre><p>I started the cleanup by removing some undoubtedly optional files like <code>~/Downloads/</code> and migrating data files to the second drive.
I managed to salvage a decent bit:</p>
<pre tabindex="0"><code>$ df -h /
/dev/mapper/vgxubuntu-root      467G  421G   23G  95% /
</code></pre><p>So where is the rest of the junk?
I was astonished when I found out that 25% of my disk space was occupied by various caches!</p>
<h2 id="the-root-of-all-evil" >The Root of All Evil
<span>
    <a href="#the-root-of-all-evil">
        <svg viewBox="0 0 28 23" height="100%" width="19" xmlns="http://www.w3.org/2000/svg"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" fill="none" stroke-linecap="round" stroke-miterlimit="10" stroke-width="2"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" fill="none" stroke-linecap="round" stroke-miterlimit="10" stroke-width="2"/></svg>
    </a>
</span>
</h2><p>I&rsquo;ve identified the three most painful <em>storage eaters</em>:</p>
<ul>
<li>Docker</li>
<li>Poetry</li>
<li>pip</li>
</ul>
<p>That&rsquo;s not a huge surprise given that I mainly focus on Python development.</p>
<h3 id="docker" >Docker
<span>
    <a href="#docker">
        <svg viewBox="0 0 28 23" height="100%" width="19" xmlns="http://www.w3.org/2000/svg"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" fill="none" stroke-linecap="round" stroke-miterlimit="10" stroke-width="2"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" fill="none" stroke-linecap="round" stroke-miterlimit="10" stroke-width="2"/></svg>
    </a>
</span>
</h3><p>I use <a href="https://docs.docker.com/get-started/">Docker</a> for various stuff, such as running legacy projects that require ancient system deps, quick testing in limited prod-like environments, or just experimenting with unsafe code.</p>
<pre tabindex="0"><code>$ docker system df
TYPE            TOTAL     ACTIVE    SIZE      RECLAIMABLE
Images          44        9         80.36GB   78.29GB (97%)
Containers      11        1         2.618GB   2.618GB (100%)
Local Volumes   47        5         1.219GB   983.4MB (80%)
Build Cache     0         0         0B        0B
</code></pre><p>You can see the full summary of your docker daemon with <a href="https://docs.docker.com/reference/cli/docker/system/df/"><code>docker system df -v</code></a>.
There&rsquo;s a <em>mostly</em> safe command for removing all stopped containers, unused networks, dangling images, and dangling build cache:</p>
<pre tabindex="0"><code>$ docker system prune
(...)
Total reclaimed space: 8.371GB
</code></pre><p>There&rsquo;s also a more aggressive option <code>-a</code>, that will also remove all images without at least one associated container and the whole build cache:</p>
<pre tabindex="0"><code>$ docker system prune -a
(...)
Total reclaimed space: 73.91GB
</code></pre><p>I decided to go with <code>-a</code> right now, to have a fresh start.
80 GB reclaimed.</p>
<h3 id="poetry" >Poetry
<span>
    <a href="#poetry">
        <svg viewBox="0 0 28 23" height="100%" width="19" xmlns="http://www.w3.org/2000/svg"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" fill="none" stroke-linecap="round" stroke-miterlimit="10" stroke-width="2"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" fill="none" stroke-linecap="round" stroke-miterlimit="10" stroke-width="2"/></svg>
    </a>
</span>
</h3><p>In 2023 we started using <a href="https://python-poetry.org/"><code>Poetry</code></a> to manage our Python environments at work.
Check it out, it&rsquo;s finally production-ready after maturing for a couple of years (the early performance issues are now gone!).</p>
<p>I couldn&rsquo;t find a built-in command to list Poetry&rsquo;s cache size.
There&rsquo;s <a href="https://python-poetry.org/docs/cli/#cache-list"><code>poetry cache list</code></a>, but it only shows cached package registry names:</p>
<pre tabindex="0"><code>$ poetry cache list
PyPI
_default_cache
secret-registry-1
secret-registry-2
(...)
</code></pre><p>By default Poetry keeps the cache in <code>~/.cache/pypoetry</code>, but it can be configured with the <code>POETRY_CACHE_DIR</code> env variable:</p>
<pre tabindex="0"><code>$ du -sh .cache/pypoetry/* | sort -h
11G	.cache/pypoetry/artifacts  # packages (wheels, tarballs)
12G	.cache/pypoetry/cache  # package metadata used to build `poetry.lock` file
99M	.cache/pypoetry/src  # this one stores source repositories; for example, when deps are specified with a git URI
21G	.cache/pypoetry/virtualenvs  # this one stores actual virtualenvs ;)
</code></pre><p>You can use <a href="https://python-poetry.org/docs/cli/#cache-clear"><code>poetry cache clear --all &lt;cache name&gt;</code></a> to delete all cache entries from a given repository.
I find it overly complicated to use:</p>
<pre tabindex="0"><code>$ poetry cache clear --all _default_cache
Delete 1208 entries? (yes/no) [yes]
</code></pre><p>There&rsquo;s <a href="https://github.com/python-poetry/poetry/issues/8156">an undocumented option</a> that lets you pass <code>.</code> as a cache name to clear all caches:</p>
<pre tabindex="0"><code>$ poetry cache clear --all .
Delete 4426 entries? (yes/no) [yes]
</code></pre><p>Unfortunately, none of those will clean the <code>artifacts</code> directory, so we&rsquo;re left with:</p>
<pre tabindex="0"><code>$ rm -r ~/.cache/pypoetry/artifacts/*
</code></pre><p>That&rsquo;s an extra 23 GB.</p>
<h3 id="legacy-pip" >Legacy pip
<span>
    <a href="#legacy-pip">
        <svg viewBox="0 0 28 23" height="100%" width="19" xmlns="http://www.w3.org/2000/svg"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" fill="none" stroke-linecap="round" stroke-miterlimit="10" stroke-width="2"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" fill="none" stroke-linecap="round" stroke-miterlimit="10" stroke-width="2"/></svg>
    </a>
</span>
</h3><p>I don&rsquo;t use <a href="https://pip.pypa.io/en/stable/"><code>pip</code></a> directly anymore since the migration to poetry.
Some leftovers were lurking in the shadows:</p>
<pre tabindex="0"><code>$ pip cache info
Package index page cache location (pip v23.3+): /home/florczak/.cache/pip/http-v2
Package index page cache location (older pips): /home/florczak/.cache/pip/http
Package index page cache size: 5398.9 MB
Number of HTTP files: 2218
Locally built wheels location: /home/florczak/.cache/pip/wheels
Locally built wheels size: 15.6 MB
Number of locally built wheels: 41
</code></pre><p>Yay, another 5 GB reclaimed.</p>
<h2 id="closing-words" >Closing Words
<span>
    <a href="#closing-words">
        <svg viewBox="0 0 28 23" height="100%" width="19" xmlns="http://www.w3.org/2000/svg"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" fill="none" stroke-linecap="round" stroke-miterlimit="10" stroke-width="2"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" fill="none" stroke-linecap="round" stroke-miterlimit="10" stroke-width="2"/></svg>
    </a>
</span>
</h2><p>The cache is crucial for optimizing performance, but an unmanaged cache leads to resource drainage.
When you design an application that heavily relies on cache let your users know the consequences.
Create cache invalidation/cleanup policies that make the most sense in your use cases.</p>
<p>I suspect that at least half of the 108 GB cache space I freed was stale and will never be populated again.
Only time will tell.</p>]]></content:encoded>
    </item>
    
    <item>
      <title>Fighting Cheaters in Call of Duty 4</title>
      <link>https://blog.andr.host/posts/2022/08/10-fighting-cheaters-in-cod-4/</link>
      <pubDate>Wed, 10 Aug 2022 00:00:00 +0000</pubDate>
      <author>florczak.raf&#43;blog@gmail.com (Rafał Florczak)</author>
      <guid>https://blog.andr.host/posts/2022/08/10-fighting-cheaters-in-cod-4/</guid>
      <description>
        <![CDATA[Cheating in multiplayer games has always been a thing that triggered me the most while playing them. During the time I was managing a game server network used by hundreds of players every day I learned some neat tricks to fight the cheaters back.
]]>
      </description>
      
        
          <category>games</category>
        
      
      <content:encoded><![CDATA[<p>Cheating in multiplayer games has always been a thing that triggered me the most while playing them.
During the time I was managing a game server network used by hundreds of players every day I learned some neat tricks to fight the cheaters back.</p>
<h2 id="ethical-disclaimer" >Ethical Disclaimer
<span>
    <a href="#ethical-disclaimer">
        <svg viewBox="0 0 28 23" height="100%" width="19" xmlns="http://www.w3.org/2000/svg"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" fill="none" stroke-linecap="round" stroke-miterlimit="10" stroke-width="2"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" fill="none" stroke-linecap="round" stroke-miterlimit="10" stroke-width="2"/></svg>
    </a>
</span>
</h2><p>This post contains information that can be used to cheat (and avoid being caught red-handed) in <a href="https://en.wikipedia.org/wiki/Call_of_Duty_4:_Modern_Warfare">Call of Duty 4</a> (CoD4) multiplayer servers that require latest official version of the game.
This also applies to servers with the default anticheat software enabled at both ends of communication.
<strong>Without</strong> a need to modify any of the game files.</p>
<p>I decided to finally publish this post because the game it&rsquo;s mostly related to has been released in 2007 and the latest official patch (version 1.7) has been released in June 2008, 14 years ago.
All the information presented here can be found on the internet anyway.</p>
<h2 id="intro" >Intro
<span>
    <a href="#intro">
        <svg viewBox="0 0 28 23" height="100%" width="19" xmlns="http://www.w3.org/2000/svg"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" fill="none" stroke-linecap="round" stroke-miterlimit="10" stroke-width="2"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" fill="none" stroke-linecap="round" stroke-miterlimit="10" stroke-width="2"/></svg>
    </a>
</span>
</h2><p>Most new games nowadays don&rsquo;t have option to let people host and manage (and often modify) their own dedicated servers.
However, a decade ago the situation was a complete opposite.
Before the age of automated matchmaking, you had to manually find and join a server to play.
Usually there were a lot of active servers but not all of them were given a top-notch care from the administrators in all matters.</p>
<p>Some issues were related to keeping the chat civilized.
Others revolved around abusing some gameplay mechanisms.
However, the worst problems were always related to detecting and removing people who cheated.
It&rsquo;s a complex topic thus I see why it&rsquo;s usually ignored or delegated to the game&rsquo;s developers and calling it a day.</p>
<p>All the information in this post is based on my knowledge gathered over the years of hosting various game servers between 2009 and 2016.
The most interesting years were definitely the final ones under the Morning Star Gaming<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup> brand, where we had access to high-end hardware that allowed us to deliver lag-free experience for thousands of unique players over the years.</p>
<p>In order to visualize some of the topics better, I fetched old backups so that I could use real-world data.
The logs I will use come from the CoD4 servers that were used for competitive gameplay between 2014-07-01 and 2015-07-01.</p>
<h2 id="punkbuster" >PunkBuster
<span>
    <a href="#punkbuster">
        <svg viewBox="0 0 28 23" height="100%" width="19" xmlns="http://www.w3.org/2000/svg"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" fill="none" stroke-linecap="round" stroke-miterlimit="10" stroke-width="2"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" fill="none" stroke-linecap="round" stroke-miterlimit="10" stroke-width="2"/></svg>
    </a>
</span>
</h2><p><a href="https://evenbalance.com/">PunkBuster</a> is anti-cheat software suite created by Even Balance back in 2000.
It was first used in Return to Castle Wolfenstein and since then in over 30 other titles including games from Battlefield, Call of Duty and Far Cry series.
CoD4 has an optional (as in possible to be forced by each game server to let people in) PunkBuster integration.</p>
<p>PunkBuster works in client-server architecture to allow real-time memory and file scans.
It also allows to stream globally issued bans from the master servers<sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup> to the participating servers.</p>
<p>PunkBuster was rarely updated, even at the peak of its popularity, therefore only the most obvious and popular cheats were possible to detect this way.
The fact that PunkBuster worked in <a href="https://en.wikipedia.org/wiki/User_space_and_kernel_space">userland</a> didn&rsquo;t help in case of more sophisticated hacks.</p>
<p>But wait, there&rsquo;s more!</p>
<h2 id="built-in-cheat-detection" >Built-in Cheat Detection
<span>
    <a href="#built-in-cheat-detection">
        <svg viewBox="0 0 28 23" height="100%" width="19" xmlns="http://www.w3.org/2000/svg"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" fill="none" stroke-linecap="round" stroke-miterlimit="10" stroke-width="2"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" fill="none" stroke-linecap="round" stroke-miterlimit="10" stroke-width="2"/></svg>
    </a>
</span>
</h2><p>There&rsquo;s an undisclosed database of known programs used for cheating that PunkBuster looks for.
They can target either the game or the anti-cheat itself.</p>
<p>You can find some examples from my logs (sorry for the MM.DD.YYYY date format but all excerpts are copied as-is) below:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>[08.15.2014 17:59:58] VIOLATION (GAMEHACK) #89182: sSs*R|neon (slot #1) Violation (GAMEHACK) #89182 [6fcfb7e796d093e7b781a94e81f10015(VALID) 89.12.208.250:28960]
</span></span><span style="display:flex;"><span>[08.28.2014 12:18:15] VIOLATION (GAMEHACK) #89182: DK&#39;NEON (slot #8) Violation (GAMEHACK) #89182 [1c8d0554b041a5bf4b81ba7657dcc329(VALID) 89.12.241.2:28960]
</span></span><span style="display:flex;"><span>[09.02.2014 17:31:45] VIOLATION (PB HACK) #139015: |G4E|SaschaDE (slot #1) Violation (PB HACK) #139015 [24cede36831db30128d586a3387319d8(VALID) 188.195.155.156:28960]</span></span></code></pre></div>
<p>First two entries show a generic <code>GAMEHACK</code> entries, the last one indicates a cheat that interfered with the PunkBuster (<code>PB HACK</code>).
All violation log lines are pretty much similar.
They consist of a timestamp, violation type, nick and game slot of a player in question, violation details (they are the same as violation type in case of these built-in detections), GUID (a unique identifier of a player derived from the game&rsquo;s CD-KEY) and client&rsquo;s IP.</p>
<h2 id="screenshots" >Screenshots
<span>
    <a href="#screenshots">
        <svg viewBox="0 0 28 23" height="100%" width="19" xmlns="http://www.w3.org/2000/svg"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" fill="none" stroke-linecap="round" stroke-miterlimit="10" stroke-width="2"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" fill="none" stroke-linecap="round" stroke-miterlimit="10" stroke-width="2"/></svg>
    </a>
</span>
</h2><p>PunkBuster offers a somewhat limited but still sometimes useful functionality of taking screenshots of player&rsquo;s screen and sending it back to the server.
It can be used, but not limited, to spot (wallhacks)[https://en.wikipedia.org/wiki/Cheating_in_online_games#World-hacking] (a class of cheats designed to reveal enemies in some ways, not necessarily making the walls invisible ;).
Unfortunately, as with other aspects of PunkBuster, this feature was designed when commonly used screen resolutions were much smaller.
As a result, we end up with a small fraction of the screen available or a decimated picture that convey no meaning.</p>
<table>
  <thead>
      <tr>
          <th style="text-align: center"><img src="https://blog.andr.host/2022/08/cod4_wallhack.png" alt="Example of a screenshot containing a wallhack." title="Example of a screenshot containing a wallhack."></th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: center">Example of a screenshot containing a wallhack.</td>
      </tr>
  </tbody>
</table>
<p>There were also hacks to disable some or all of the screenshot functionalities.
Beside the hacks there were also bugs in game, DirectX or PunkBuster itself that prevented taking proper screenshots.</p>
<table>
  <thead>
      <tr>
          <th style="text-align: center"><img src="https://blog.andr.host/2022/08/cod4_black_screen.png" alt="Black screen." title="Black screen."></th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: center">Black screen. Either a bug in game, PunkBuster or a deliberate doing.</td>
      </tr>
  </tbody>
</table>
<table>
  <thead>
      <tr>
          <th style="text-align: center"><img src="https://blog.andr.host/2022/08/cod4_foobar_screenshot.png" alt="Desktop screenshot." title="Desktop screenshot."></th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: center">Screenshot of my foobar2000 instead of the game.</td>
      </tr>
  </tbody>
</table>
<table>
  <thead>
      <tr>
          <th style="text-align: center"><img src="https://blog.andr.host/2022/08/cod4_screenshot_blocker.png" alt="Example of a screenshot blocker." title="Example of a screenshot blocker."></th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: center">Example of a screenshot blocker that hides player info.</td>
      </tr>
  </tbody>
</table>
<p>Examples of other cheats found with screenshots:</p>
<table>
  <thead>
      <tr>
          <th style="text-align: center"><img src="https://blog.andr.host/2022/08/cod4_aimhack.png" alt="Aimhack." title="Aimhack."></th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: center">Aimhack.</td>
      </tr>
  </tbody>
</table>
<table>
  <thead>
      <tr>
          <th style="text-align: center"><img src="https://blog.andr.host/2022/08/cod4_crosshair_hack.png" alt="Crosshair hack." title="Crosshair hack."></th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: center">Crosshair hack, probably just an all-in-one hack because the line on the right is most likely a part of a bounding box around enemy.</td>
      </tr>
  </tbody>
</table>
<table>
  <thead>
      <tr>
          <th style="text-align: center"><img src="https://blog.andr.host/2022/08/cod4_custom_textures.png" alt="Custom textures." title="Custom textures."></th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: center">Custom textures are indicator of modified game files. There&rsquo;s also an ESP (Extra Sensory Perception) hack that shows info about enemies other than just their position.</td>
      </tr>
  </tbody>
</table>
<table>
  <thead>
      <tr>
          <th style="text-align: center"><img src="https://blog.andr.host/2022/08/cod4_hack_config.png" alt="Hack config." title="Hack config."></th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: center">Hack config.</td>
      </tr>
  </tbody>
</table>
<table>
  <thead>
      <tr>
          <th style="text-align: center"><img src="https://blog.andr.host/2022/08/cod4_radar_hack.png" alt="Radar hack." title="Radar hack."></th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: center">Radar hack. Positions of all enemies are always shown on the radar, not only when scanned by in-game UAV or when shooting without a silencer. Achievable with setting read-only <code>g_CompassShowEnemies</code> to <code>1</code>.</td>
      </tr>
  </tbody>
</table>
<table>
  <thead>
      <tr>
          <th style="text-align: center"><img src="https://blog.andr.host/2022/08/cod4_mombot.png" alt="Mombot." title="Mombot."></th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: center">An overlay of a <code>Mombot</code> hack.</td>
      </tr>
  </tbody>
</table>
<h2 id="cvar-violations" >CVAR Violations
<span>
    <a href="#cvar-violations">
        <svg viewBox="0 0 28 23" height="100%" width="19" xmlns="http://www.w3.org/2000/svg"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" fill="none" stroke-linecap="round" stroke-miterlimit="10" stroke-width="2"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" fill="none" stroke-linecap="round" stroke-miterlimit="10" stroke-width="2"/></svg>
    </a>
</span>
</h2><p>Now we can move on to the less known features of PunkBuster.
As an administrator of a server you can also define a list of game variables (CVARs) to scan periodically.
They can be set in couple different modes: value range (both in and out) as well as string inclusion/exclusion.</p>
<p>Here&rsquo;s a general definition from the docs and an excerpt from the config file we used back in the days (<code>;</code> denotes a comment in PunkBuster config syntax):</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>;PB_SV_Cvar [Cvar_name] [IN/OUT/INCLUDE/EXCLUDE] [Param1] [optional_Param2]
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>;melee checks
</span></span><span style="display:flex;"><span>pb_sv_cvar aim_automelee_lerp IN 40
</span></span><span style="display:flex;"><span>pb_sv_cvar aim_automelee_region_height IN 240
</span></span><span style="display:flex;"><span>pb_sv_cvar aim_automelee_region_width IN 320
</span></span><span style="display:flex;"><span>pb_sv_cvar aim_automelee_enabled IN 1
</span></span><span style="display:flex;"><span>pb_sv_cvar aim_automelee_range IN 128
</span></span><span style="display:flex;"><span>pb_sv_cvar aim_automelee_debug IN 0
</span></span><span style="display:flex;"><span>pb_sv_cvar player_meleerange IN 64
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>;disabled anti-aliasing (more on that later!)
</span></span><span style="display:flex;"><span>pb_sv_cvar r_aasamples IN 1
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>;misc
</span></span><span style="display:flex;"><span>pb_sv_cvar developer IN 0
</span></span><span style="display:flex;"><span>pb_sv_cvar sv_cheats IN 0
</span></span><span style="display:flex;"><span>pb_sv_cvar g_compassshowenemies IN 0
</span></span><span style="display:flex;"><span>pb_sv_cvar r_fullbright IN 0
</span></span><span style="display:flex;"><span>pb_sv_cvar com_maxfps IN 40 333</span></span></code></pre></div>
<p>This method has worked great for some time.
It allowed us to find cheaters that used cvar unlocking hacks – programs that basically removed the read-only status from some variables which values, when changed, could give someone an unfair advantage.</p>
<p>And guess what?
CoD4 has a built-in method of changing the read-only variables!
It&rsquo;s possible through a side-effect of a quite complicated scripting function for loading translations from files to variables.
Namely, <code>SelectStringTableEntryInDvar foo foo sv_cheats 1</code> command would fail to find a <code>foo</code> in <code>foo</code> so it would just forcibly set <code>sv_cheats</code> to <code>1</code> despite the variable being set as read-only.
This bug hasn&rsquo;t been officially patched.
And as far as I can tell, it even made to the next installment of the game!</p>
<p>Below you can find some examples of variables violations and stats from the period covered by the logs:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>[10.16.2014 20:15:04] VIOLATION (CVAR) #9001: testing stuff (slot #1) Cvar sv_cheats = 1 [db4a323740c24b9a31915c3e636e8fdb(VALID) 217.99.224.187:28960]
</span></span><span style="display:flex;"><span>[07.15.2014 18:48:58] VIOLATION (CVAR) #9001: mM#neon (slot #2) Cvar developer = 1 [c108d2381c62d8e0aadc17ffa941bfc2(VALID) 89.13.148.61:28960]
</span></span><span style="display:flex;"><span>[09.22.2014 18:41:58] VIOLATION (CVAR) #9001: Dougmau5 (slot #7) Cvar r_fullbright = 1 [9ed7ac09b6b429a2f04357d3419a658d(VALID) 81.131.93.97:28960]
</span></span><span style="display:flex;"><span>[09.02.2014 15:36:00] VIOLATION (CVAR) #9001: Fire (slot #3) Cvar aim_automelee_range = 255 [babaa765e4257430197cce9b0c8a008a(VALID) 31.183.36.255:55195]
</span></span><span style="display:flex;"><span>[09.18.2014 13:36:49] VIOLATION (CVAR) #9001: spec (slot #1) Cvar cl_yawspeed = 22500 [de20c946db20b3f64758ee24b48c4c23(VALID) 89.14.116.144:28960]
</span></span><span style="display:flex;"><span>[08.12.2014 12:57:59] VIOLATION (CVAR) #9001: LS|Wojtix. (slot #8) Cvar j_lodScaleRigid = 4 [cf108bcb2a4ca15408e5b78e79d5a62e(VALID) 89.230.231.36:28960]
</span></span><span style="display:flex;"><span>[09.27.2014 21:34:08] VIOLATION (CVAR) #9001: Mokova (slot #5) Cvar fx_enable = 0 [01174c7e83fd6cc9a52db198899ac0c9(VALID) 96.22.12.28:28960]
</span></span><span style="display:flex;"><span>[08.27.2014 13:02:21] VIOLATION (CVAR) #9001: sSs*|Asqwe:3 (slot #1) Cvar cg_hudDamageIconHeight = 0 [ffa81a6a32c10f23b68a6e65f87512d8(VALID) 86.111.104.134:28960]
</span></span><span style="display:flex;"><span>[08.29.2014 18:57:15] VIOLATION (CVAR) #9001: RAGE HorNed (slot #5) Cvar phys_bulletSpinScale = -1 [2bfe80aa39ec04249a2897370f7301c5(VALID) 95.52.34.145:28960]
</span></span><span style="display:flex;"><span>[09.16.2014 00:22:20] VIOLATION (CVAR) #9001: ^3[^0ms^3]^0nor (slot #1) Cvar cl_forceavidemo = 1 [9126e989c56bfbc92307082716646ab8(VALID) 86.16.229.77:28960]
</span></span><span style="display:flex;"><span>[08.21.2014 22:00:25] VIOLATION (CVAR) #9001: =sQ=TEST CFG GG (slot #7) Cvar cg_nomip = 128 [4e7dbd87cf2bb93d7359334939aa463d(VALID) 193.106.161.132:26456]
</span></span><span style="display:flex;"><span>[08.12.2014 15:15:13] VIOLATION (CVAR) #9001: neon (slot #8) Cvar phys_bulletUpBias = 0 [6fcfb7e796d093e7b781a94e81f10015(VALID) 89.12.93.110:28960]
</span></span><span style="display:flex;"><span>[07.08.2014 13:02:06] VIOLATION (CVAR) #9001: M][K*knifer (slot #5) Cvar cl_punkbuster = 0 [89377397ef9633e2b30c1f0f7ad02e3d(VALID) 81.241.85.246:28960]</span></span></code></pre></div>
<ul>
<li>Total CVAR violations: 994</li>
<li>Unique CVAR violators: 296</li>
</ul>
<p>Top offenders:
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>     62 8b2fcad7b931f60ebfff1631ab97562a
</span></span><span style="display:flex;"><span>     29 6fcfb7e796d093e7b781a94e81f10015
</span></span><span style="display:flex;"><span>     26 5f58a8d513865b56df363d380d72f71d
</span></span><span style="display:flex;"><span>     21 ffa81a6a32c10f23b68a6e65f87512d8
</span></span><span style="display:flex;"><span>     20 3d689aa282bf6ade29d809f4e8ec7c1b</span></span></code></pre></div></p>
<p><code>r_aasamples</code> equal to <code>1</code> means that the anti-aliasing feature in the game&rsquo;s renderer is disabled.
It became a problem in newer Windows versions when it came to screenshots taken by PunkBuster.
All of them came back pink when anti-aliasing was enabled so we decided to disallow it altogether.</p>
<table>
  <thead>
      <tr>
          <th style="text-align: center"><img src="https://blog.andr.host/2022/08/cod4_pink_screen.png" alt="Pink screen." title="Pink screen."></th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: center">Pink screen caused by enabled anti-aliasing on Windows 8.</td>
      </tr>
  </tbody>
</table>
<p>There&rsquo;s also an on-demand version of this functionality that allows you to just peek at the values without issuing kicks or bans.
It was useful early on, to collect as much data about cheaters as possible without alerting them.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>[01.03.2015 18:29:23] Malfunctioning PB Client (slot 12) - Ignoring Cvar Queries
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>[07.29.2014 10:53:58] 5 Cvar Queries Sent
</span></span><span style="display:flex;"><span>[07.29.2014 10:53:58] [From #1 b7e8(VALID) {RR}andr] ai_meleerange = &#34;64&#34;
</span></span><span style="display:flex;"><span>[07.29.2014 10:53:58] [From #7 143b(VALID) =sQ= Savie] ai_meleerange = &#34;99999999&#34;
</span></span><span style="display:flex;"><span>[07.29.2014 10:53:58] [From #3 54b4(VALID) *LEU*Romeo] ai_meleerange = &#34;64&#34;
</span></span><span style="display:flex;"><span>[07.29.2014 10:53:59] [From #4 2c81(VALID) [VS-UK]psychOl] ai_meleerange = &#34;99999999&#34;
</span></span><span style="display:flex;"><span>[07.29.2014 10:53:59] [From #2 c664(VALID) =sQ= SKiLLzZ] ai_meleerange = &#34;64&#34;</span></span></code></pre></div>
<p>Do you remember when I said that this method worked great for some time?
It was fine until cheaters learned that they could modify strings in game&rsquo;s executable to use different names for the variables we were enforcing values for.
For example, the variable <code>aim_automelee_ranga</code> should not exist at all because it&rsquo;s real name is supposed to be <code>aim_automelee_range</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>[07.06.2014 19:26:01] VIOLATION (CVAR) #9001: god#D1veR (slot #1) Cvar aim_automelee_ranga = 155 [f289fb8c7131e39cafc4fe6c8552d040(VALID) 81.4.194.85:28960]</span></span></code></pre></div>
<p>But how did we even realize this was the case?</p>
<h2 id="bind-scans" >Bind Scans
<span>
    <a href="#bind-scans">
        <svg viewBox="0 0 28 23" height="100%" width="19" xmlns="http://www.w3.org/2000/svg"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" fill="none" stroke-linecap="round" stroke-miterlimit="10" stroke-width="2"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" fill="none" stroke-linecap="round" stroke-miterlimit="10" stroke-width="2"/></svg>
    </a>
</span>
</h2><p>Lo and behold, here come the bind scans.
This little functionality allowed us to poll players&rsquo; configs for known game actions assigned (or &ldquo;bound&rdquo; in gamers jargon) to keys.
On some occasion they also revealed illegal macros used to perform actions in sequence that were impossible otherwise.</p>
<p>Cheaters sometimes assigned extra stuff to the normal movement or attack keys.
That way we could reveal some of the custom variable names that their modified game clients actually used.</p>
<p>I&rsquo;m sorry but I couldn&rsquo;t find full logs, only partial messages from admins communication because bind scans were performed on-demand and weren&rsquo;t logged to the main log file.
Here&rsquo;s what I managed to salvage for the sake of this post:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>[07.01.2015 18:04:14] [From #4 9ee6(VALID) rOSTA|FFF] [Q = weapnext; wait 2; weapprev; wait 5; +melee; com_maxfps 80; -melee;wait 33; com_maxfps 333; wait 18; +activate; wait 2; -activate][1 key binding match found]
</span></span><span style="display:flex;"><span>[07.01.2015 18:04:53] [From #4 9ee6(VALID) rOSTA|FFF] [3 = zzz_popokills_mamba 129][W = +forward;z_loh 0;zz_kokokillsmamba 0;zz_tutu 0;zz_lalala 0;sv_clientsidebullets 0;r_znear 30;r_znear_depthhack 4;bg_bobmax 0;fx_drawclouds 0][2 key binding matches found]
</span></span><span style="display:flex;"><span>[07.24.2014 12:24:29] [From #10 426f(VALID) dUb crizis] [E = +melee][KEY_F3 = aim_automelee_ranga 165][KEY_F4 = aim_automelee_ranga 0][3 key binding matches found]
</span></span><span style="display:flex;"><span>[07.07.2014 16:10:36] [From #7 2543(VALID) BT#XtronS] [F = +activate][V = weapnext; wait 2; weapprev; wait 5; +melee; com_maxfps 125; -melee; wait 40; com_maxfps 333; wait 18; +activate; wait 2; -activate][2 key binding matches found]
</span></span><span style="display:flex;"><span>Z = &#34;SelectStringTableEntryInDvar FPS FPS r_fullbright 1&#34;</span></span></code></pre></div>
<p>I can assure that neither <code>zzz_popokills_mamba</code> nor <code>zz_lalala</code> or even <code>zz_kokokillsmamba</code> are built-in variables ;)
They are, however, matching the lengths of their original names because they were patched directly in game&rsquo;s binary without changing any of the program&rsquo;s logic.</p>
<p>So how do we deal with those modified clients?</p>
<h2 id="md5tool" >MD5Tool
<span>
    <a href="#md5tool">
        <svg viewBox="0 0 28 23" height="100%" width="19" xmlns="http://www.w3.org/2000/svg"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" fill="none" stroke-linecap="round" stroke-miterlimit="10" stroke-width="2"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" fill="none" stroke-linecap="round" stroke-miterlimit="10" stroke-width="2"/></svg>
    </a>
</span>
</h2><p>The last tool in our arsenal is a mechanism that allows to run periodic checksum scans on files in player&rsquo;s game directory.
Unfortunately, the limits imposed on the frequency and size of scans were the major PITA.
You can check at most 2048 bytes in a single scan, every 30 seconds.
Just to scan a relatively small – 3330048 bytes – game executable you&rsquo;d need a player to stay on the server for 12 hours<sup id="fnref:3"><a href="#fn:3" class="footnote-ref" role="doc-noteref">3</a></sup>.
There are also other game files that could use some scanning, for example asset packs, to eliminate alteration of textures (that could lead, for instance, to walls or grass transparency).</p>
<p>In order to overcome the limitations I first generated a sequence of the checksums on a vanilla game.
Then I modified the game executable to alter the variable names I deemed the most important for us and generated another set of checksums.
Final lists were then diffed and I came up with a final list of 15 offsets that could be scanned in 7 minutes:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>;generated by andr (2014-10-10 18:41)
</span></span><span style="display:flex;"><span>;iw3mp.exe v1.6
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>pb_sv_md5toolempty
</span></span><span style="display:flex;"><span>pb_sv_md5toolfreq 30
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>pb_sv_md5tool a &#34;&#34; v iw3mp.exe SZ3330048 AT2738176 LEN2048 928C325DC37E804F3C77B6D2E46FF294
</span></span><span style="display:flex;"><span>pb_sv_md5tool a &#34;&#34; v iw3mp.exe SZ3330048 AT2926592 LEN2048 B2C3332CBA37E374C867883AB179F68D
</span></span><span style="display:flex;"><span>pb_sv_md5tool a &#34;&#34; v iw3mp.exe SZ3330048 AT2914304 LEN2048 4E833D47539C77F6E6597467745DE83E
</span></span><span style="display:flex;"><span>pb_sv_md5tool a &#34;&#34; v iw3mp.exe SZ3330048 AT2959360 LEN2048 7B5555B4E8ECE6D9ECAFD4D93CF88B10
</span></span><span style="display:flex;"><span>pb_sv_md5tool a &#34;&#34; v iw3mp.exe SZ3330048 AT2934784 LEN2048 70CD05CA9BE840A07B3E8024A2CBC5B8
</span></span><span style="display:flex;"><span>pb_sv_md5tool a &#34;&#34; v iw3mp.exe SZ3330048 AT2947072 LEN2048 FC8D1E1A568BE2C639E03BDC7C179061
</span></span><span style="display:flex;"><span>pb_sv_md5tool a &#34;&#34; v iw3mp.exe SZ3330048 AT2951168 LEN2048 F8D39DF25CB8ECF3752162A6BCCD6F97
</span></span><span style="display:flex;"><span>pb_sv_md5tool a &#34;&#34; v iw3mp.exe SZ3330048 AT2953216 LEN2048 BE4B6F1A3E6CF5F395D7C703BA373D29
</span></span><span style="display:flex;"><span>pb_sv_md5tool a &#34;&#34; v iw3mp.exe SZ3330048 AT2992128 LEN2048 CD114E133DB909032914F846A0D041BA
</span></span><span style="display:flex;"><span>pb_sv_md5tool a &#34;&#34; v iw3mp.exe SZ3330048 AT3020800 LEN2048 ED047C9BFD0EE6068B3FA48E8BB17611
</span></span><span style="display:flex;"><span>pb_sv_md5tool a &#34;&#34; v iw3mp.exe SZ3330048 AT3026944 LEN2048 5063B2EE918E3DF1CE90CF51830A43EF
</span></span><span style="display:flex;"><span>pb_sv_md5tool a &#34;&#34; v iw3mp.exe SZ3330048 AT3061760 LEN2048 FD246F355BCD68D85126833A1D93F670
</span></span><span style="display:flex;"><span>pb_sv_md5tool a &#34;&#34; v iw3mp.exe SZ3330048 AT3115008 LEN2048 90160CB306D7F13D2C8F08F272ABD2A9
</span></span><span style="display:flex;"><span>pb_sv_md5tool a &#34;&#34; v iw3mp.exe SZ3330048 AT3168256 LEN2048 34D42E2241D52143CE8707CA9A95E0DE
</span></span><span style="display:flex;"><span>pb_sv_md5tool a &#34;&#34; v iw3mp.exe SZ3330048 AT3162112 LEN2048 FEE4B1D001F2DFEA1C3A0C7F394D0E26</span></span></code></pre></div>
<p>An excerpt from logs that indicates MD5Tool violations:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>[10.11.2014 16:41:47] VIOLATION (MD5TOOL) #9002: cg_lan 1 (slot #1) MD5Tool Mismatch: iw3mp.exe (len=2048) [2b9ab1fbc7cdc8ee11a2a26249b4413e(VALID) 195.242.125.179:28960]
</span></span><span style="display:flex;"><span>[10.11.2014 20:50:00] VIOLATION (MD5TOOL) #9002: BASEDGOD (slot #4) MD5Tool Mismatch: iw3mp.exe (len=2048) [6cfb244345650db8b2837bf151ff8742(VALID) 107.209.190.143:28965]
</span></span><span style="display:flex;"><span>[07.05.2014 15:18:21] VIOLATION (MD5TOOL) #9002: [ms] (slot #1) MD5Tool Mismatch: main/iw_13.iwd (len=2048) [af3d2e82f247c3be99a17f9ffc873c30(VALID) 83.85.114.231:28960]
</span></span><span style="display:flex;"><span>[07.05.2014 15:21:24] VIOLATION (MD5TOOL) #9002: [ms] (slot #1) MD5Tool Mismatch: main/iw_05.iwd (len=2048) [af3d2e82f247c3be99a17f9ffc873c30(VALID) 83.85.114.231:28960]</span></span></code></pre></div>
<ul>
<li>Total MD5Tool violations: 294</li>
<li>Unique MD5Tool violators: 49</li>
</ul>
<p>Top offenders:
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>     26 1f97d04c79e3cc150232cb8d196843bb
</span></span><span style="display:flex;"><span>     22 e4afe5a995f9701a6e29dace7d109348
</span></span><span style="display:flex;"><span>     15 68a1304d02b53d6a62003dc7337639c4
</span></span><span style="display:flex;"><span>     15 26956b1198b880bd6001eece83dea4a0
</span></span><span style="display:flex;"><span>     14 9126e989c56bfbc92307082716646ab8</span></span></code></pre></div></p>
<p>Unfortunately, as with countless other anti-cheat measures, this one is particuarly easy to overcome.
The message that is shown in logs was also present in a message on the automatically issued &ldquo;kick&rdquo; when the violation was detected.
As you probably figured out by now, running the game from <code>foo.exe</code>, while keeping unmodified <code>iw3mp.exe</code> at the same time, would let you fly under the radar.</p>
<h2 id="closing-words" >Closing Words
<span>
    <a href="#closing-words">
        <svg viewBox="0 0 28 23" height="100%" width="19" xmlns="http://www.w3.org/2000/svg"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" fill="none" stroke-linecap="round" stroke-miterlimit="10" stroke-width="2"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" fill="none" stroke-linecap="round" stroke-miterlimit="10" stroke-width="2"/></svg>
    </a>
</span>
</h2><p>It was a fun couple of years I spent doing work that&rsquo;s barely visible but much needed by the community.
However, I still don&rsquo;t get it why would anyone want to cheat in a game.
What&rsquo;s to gain?</p>
<p>Dealing with those people, especially after they&rsquo;ve been caught red-handed, was draining a lot of energy.
Constant &ldquo;ban appeals&rdquo;<sup id="fnref:4"><a href="#fn:4" class="footnote-ref" role="doc-noteref">4</a></sup> and comebacks with new game copies and names, just to do same shit over and over again…</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>Morning Star Gaming, MSG for short, was a community that evolved from Knives Out CoD4 clan, that originally revolved around a specific game mode that allowed players to use only melee weapons to kill their enemies.
At the peak of its active years MSG hosted dozens of game servers in multiple games including ones from Call of Duty series, Minecraft and Counter Strike as well as forums, voice and chat services.
MSG disbanded somewhere in 2017 or 2018.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p>Master servers are usually some special servers that coordinate some operation.
In this case they are responsible for relaying global ban info and update information to game servers.
In games, master servers are often used to gather heartbeats from all active game servers in order to later display a complete server list to players.&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:3">
<p>Assuming this method would work continuously between map changes in game, which I&rsquo;m not convinced it would.
Alternatively, you&rsquo;d have to set suuuuuch loooong rounds…&#160;<a href="#fnref:3" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:4">
<p>I&rsquo;ve always advised against giving second chances to proven cheaters.
Not that it helped a lot…
They came back anyway and cheated again, with no remorse or any resocialization.
For some reason bans (and later unbans) are more likely to work as expected in case of non-cheating violations, like a use of improper language in chat or nicknames.
It has probably something to do with the irreversibly broken minds that do not use logic to make decisions.
Sometimes I wonder whether people who cheat in video games are also people who steal, extort or do other illegal shit because they are completely deranged.
I guess I will have to add psychology studies to my TODO list to find out.&#160;<a href="#fnref:4" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>]]></content:encoded>
    </item>
    
    <item>
      <title>Transfer 500GB Instead Of 70GB To Save Time</title>
      <link>https://blog.andr.host/posts/2022/06/24-p2p-power/</link>
      <pubDate>Fri, 24 Jun 2022 00:00:00 +0000</pubDate>
      <author>florczak.raf&#43;blog@gmail.com (Rafał Florczak)</author>
      <guid>https://blog.andr.host/posts/2022/06/24-p2p-power/</guid>
      <description>
        <![CDATA[A month ago I needed to download a quite big package from the Internet Archive. Unfortunately their HTTP servers throttled the connections to ~300KB/s in case of the files I was interested in. Luckily, they also offer option to use BitTorrent protocol to transfer files.
]]>
      </description>
      
        
          <category>misc</category>
        
      
      <content:encoded><![CDATA[<p>A month ago I needed to download <a href="https://archive.org/download/junos-srxsme/">a quite big package</a> from the <a href="https://archive.org/">Internet Archive</a>.
Unfortunately their HTTP servers throttled the connections to ~300KB/s in case of the files I was interested in.
Luckily, they also offer option to use BitTorrent protocol to transfer files.</p>
<p>Downloading 70GB would take roughly 3 days at 300KB/s.
I knew I could do better than that.
But I didn&rsquo;t want to circumvent any mechanisms they put in place to protect their infrastructure from people like me.</p>
<p>I was sceptic at first but I started my qBittorrent and loaded the torrent from the website.
To my surprise, there were two HTTP mirrors instead of one.
That&rsquo;s twice the original speed already!
There were no other peers at the time.</p>
<p>My theory was that everything would speed up substantially if more people were to join because the HTTP mirrors are artificially slowed down.
So I invited some friends to the experiment.
After an hour there were 7 of us downloading different parts at &ldquo;full&rdquo; speed from the Archive mirrors and sharing with the rest.</p>
<p>Some time later one of the upstream hosts went down but I managed to download the whole thing in under 8 hours.
One of the friends keeps seeding it on his server and reached the ratio of 9.7 over last month.
So there <em>are</em> other people interested in this package after all.</p>
<p>Shout-outs to <a href="https://aquaz.eu">aquaz</a>, <a href="https://twitter.com/dexterw68">dexterw</a>, <a href="https://halamix2.pl">Halamix2</a>, <a href="https://twitter.com/kaadzik">Kaadzik</a>,
<a href="https://queenspalace.dev">pitpo</a> and <a href="https://pokerfacowaty.com">PokerFacowaty</a> for help.</p>]]></content:encoded>
    </item>
    
    <item>
      <title>TIL: Some Apache Config Options</title>
      <link>https://blog.andr.host/posts/2022/06/13-apache-config-changes/</link>
      <pubDate>Mon, 13 Jun 2022 00:00:00 +0000</pubDate>
      <author>florczak.raf&#43;blog@gmail.com (Rafał Florczak)</author>
      <guid>https://blog.andr.host/posts/2022/06/13-apache-config-changes/</guid>
      <description>
        <![CDATA[I run Apache as my main web server. There&rsquo;s rarely a need to modify anything but when there is, it&rsquo;s usually quite simple. That&rsquo;s mainly why I never bothered to switch to nginx. Over the course of last month I had to change two things, here&rsquo;s a short summary.
]]>
      </description>
      
        
          <category>TIL</category>
        
      
      <content:encoded><![CDATA[<p>I run Apache as my main web server.
There&rsquo;s rarely a need to modify anything but when there is, it&rsquo;s usually quite simple.
That&rsquo;s mainly why I never bothered to switch to nginx.
Over the course of last month I had to change two things, here&rsquo;s a short summary.</p>
<p>I wanted to share some markdown (<code>.md</code>) files but my configuration forced download (or at least didn&rsquo;t request in-lining) even though it&rsquo;s just a text file that could be rendered in the browser.
I learned that this behavior depends on <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition"><code>Content-Disposition</code></a> HTTP header.
Apache has a module <code>mod_headers</code> that allows setting arbitrary headers, along with <code>FilesMatch</code> I came up with the following solution:</p>
<pre tabindex="0"><code>&lt;IfModule mod_headers.c&gt;
  &lt;FilesMatch &#34;.+\.md$&#34;&gt;
    Header set Content-Disposition inline
    Header set Content-Type text/plain
  &lt;/FilesMatch&gt;
&lt;/IfModule&gt;
</code></pre><p>Today I wanted to add a static files hosting to a VirtualHost that was serving as a reverse proxy until now.
I knew about <code>Alias</code> directive but it didn&rsquo;t work out of the box — the requests for aliased path
were still proxied to an application that didn&rsquo;t know how to handle them.
It turned out that the priority of <code>ProxyPass</code> is higher than <code>Alias</code> so I had to explicitly write an exclusion for the aliased directory like so:</p>
<pre tabindex="0"><code>(...)
ProxyPass /static !
Alias /static /path/to/my/static
ProxyPass / http://127.0.0.1:1337/
ProxyPassReverse / http://127.0.0.1:1337/
(...)
</code></pre>]]></content:encoded>
    </item>
    
    <item>
      <title>Drivers in Windows</title>
      <link>https://blog.andr.host/posts/2022/05/28-drivers-in-windows/</link>
      <pubDate>Sat, 28 May 2022 00:00:00 +0000</pubDate>
      <author>florczak.raf&#43;blog@gmail.com (Rafał Florczak)</author>
      <guid>https://blog.andr.host/posts/2022/05/28-drivers-in-windows/</guid>
      <description>
        <![CDATA[Windows. Huh. I didn&rsquo;t anticipate me writing about Windows here. Thankfully, it&rsquo;s going to be a rant about drivers. I&rsquo;ve got three short stories that all happened somewhere earlier this year. The first one chronologically is about one of the computers at work.
]]>
      </description>
      
        
          <category>rant</category>
        
      
      <content:encoded><![CDATA[<p>Windows. Huh.
I didn&rsquo;t anticipate me writing about Windows here.
Thankfully, it&rsquo;s going to be a rant about drivers.
I&rsquo;ve got three short stories that all happened somewhere earlier this year.
The first one chronologically is about one of the computers at work.</p>
<p>Long story short — we wanted to test whether Linux was a problem or not<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup> so we installed Windows 10.
Installation took couple minutes, new system booted from the NVMe drive, so far so good.
There was one little issue, the network didn&rsquo;t work.
The on-board LAN (some popular Intel chip) was detected but it was unable to negotiate any link.
Some generic Microsoft-provided driver was loaded…</p>
<p>It&rsquo;s 2022 and the system is dysfunctional after the fresh installation.
What if someone didn&rsquo;t have a second computer to fetch and transfer the driver via the USB stick?
Less and less PC cases include 5.25″ bays<sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup> so the provided driver CDs are only a piling trash.</p>
<p>Second story is also going to be about a network adapter driver but on my private computer.
Recently I changed my ISP to the one that offered a FTTH so I could upgrade from the old unstable 150/15 Mbps cable<sup id="fnref:3"><a href="#fn:3" class="footnote-ref" role="doc-noteref">3</a></sup>.
The technician did his thing in 1.5h and left.
It was around 11:30 so I was still working but I &ldquo;risked&rdquo; switching to the new connection.
The speeds were oscillating around the expected values (943/301 Mbps averaged over three speedtests) on Linux.
After work I switched to Windows to, you know, maintain the mythical work-life balance<sup id="fnref:4"><a href="#fn:4" class="footnote-ref" role="doc-noteref">4</a></sup>.</p>
<p>First thing I did was another speedtest, just in case.
I observed the pointer of the speedometer going rapidly up.
200, 300, 500, 700, 800. BAM.
Both my screens went black for a second.
Huh. Strange. Can&rsquo;t be related to the new internet, can it?
I fired up another speedtest.
300, 700, 800. BAM. Black screens again but also recovered quickly.
When I checked the Event Viewer, I found out that the black screens were related to the Nvidia driver crash.</p>
<p>I started yet another speedtest but this time I also opened Task Manager to take a look at resource usage.
To my surprise both CPU and GPU were going crazy high on some system daemon when the test was accelerating.
I didn&rsquo;t have high hopes but I tried to update my Nvidia driver… and the system crashed while downloading it.
This time the screens didn&rsquo;t come back without a reboot.
Then it suddenly dawned on me that it must be related to the crappy MS-provided drivers for the network adapter.
I manually overrode the link speed to 100 Mbps and downloaded a driver from the mobo manufacturer.</p>
<p>Et voilà, all my problems solved.
This time the generic driver &ldquo;worked&rdquo; and I didn&rsquo;t realize that it was not offloading to the chip for almost a year thanks to my old internet connection.
I&rsquo;d probably found out about it sooner if I had less powerful components in my PC.
But hey, at least now we know that a 10th gen i9 and an RTX2070S are not capable of calculating checksums at gigabit speeds using the generic driver.
This time I wished it didn&rsquo;t work at all.</p>
<p>Third story is going to revolve around a HDD.
In January I bought a new 8TB HDD<sup id="fnref:5"><a href="#fn:5" class="footnote-ref" role="doc-noteref">5</a></sup>, formatted it under Windows to use NTFS and Bitlocker so I could use it from Linux as well.
The last time I checked, Windows still didn&rsquo;t support ext4 and LUKS out of the box :(.
Everything looked good so far.
This unfortunately changed when I started to copy files onto the new disk.
Write speeds were never exceeding ~30 MBps flat on Windows with huge files.
I couldn&rsquo;t resist and checked how it behaves on Linux.
Despite a weird filesystem and Bitlocker, the drive could maintain a stable 110+ MBps writes there.</p>
<p>I did some googling but nothing useful or even remotely connected came up so I gave up.
It was just Windows doing windowsy things all over again.
The experiment showed that the drive clearly wasn&rsquo;t an issue here.
I could live with those speeds because it&rsquo;s going to be used only for archiving purpose.
However, this weekend while I browsed the Device Manager I found out that the SATA controller was listed as <code>Standard SATA AHCI Controller</code> and used the MS-provided driver…
So, I hope you know the drill by now.</p>
<p>The thing that pains me the most is that it&rsquo;s 2022 already and Microsoft still doesn&rsquo;t ship proper drivers for common hardware.
And that their automatic updates fail to update the drivers to match the devices properly.
Ok, ok. Most people probably wouldn&rsquo;t notice the 3rd issue I presented <em>but it&rsquo;s still there</em>.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>Surprise, surprise! It was not.
System literally froze randomly.
Sometimes after an one hour, sometimes after 42 or 69 since the start.
No logs, no other indicators from the monitoring either.
It turned out to be a hardware mobo issue but the seller refused to replace/fix it because they &ldquo;couldn&rsquo;t reproduce the issue&rdquo;.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p>I scouted a popular consumer-grade PC part shop.
They offered 707 PC cases, out of which 504 had no 5.25″ bays, 97 had one and 106 had two or more.&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:3">
<p>This unresolved mystery deserves a post on its own. Adding to the TODO list.&#160;<a href="#fnref:3" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:4">
<p>I have a dual-boot setup with Debian being my daily work and hobby driver and Windows for stuff that doesn&rsquo;t work on Linux.
For instance, that last group includes streaming games to Oculus via WiFi.
Dual boot significance will come up once again later so stay tuned.&#160;<a href="#fnref:4" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:5">
<p>It was my first drive to use GPT because all other drives could function with MBR so I didn&rsquo;t bother exploring GPT.
I think it had nothing to do with the whole situation but I just felt like sharing this.&#160;<a href="#fnref:5" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>]]></content:encoded>
    </item>
    
    <item>
      <title>All Your Radio Are Belong To Us</title>
      <link>https://blog.andr.host/posts/2022/05/26-iii-radio/</link>
      <pubDate>Thu, 26 May 2022 00:00:00 +0000</pubDate>
      <author>florczak.raf&#43;blog@gmail.com (Rafał Florczak)</author>
      <guid>https://blog.andr.host/posts/2022/05/26-iii-radio/</guid>
      <description>
        <![CDATA[Grand Theft Auto III is the first game in the 3D universe1 of the series. It lets you cruise around Liberty City in various vehicles. Most of them are equipped with regular radios. However, there are two sets of vehicles that either don&rsquo;t have radios or override the radio station with some random police chatter.
]]>
      </description>
      
        
          <category>games</category>
        
          <category>reverse-engineering</category>
        
      
      <content:encoded><![CDATA[<p>Grand Theft Auto III is the first game in the 3D universe<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup> of the <a href="https://en.wikipedia.org/wiki/Grand_Theft_Auto">series</a>.
It lets you cruise around Liberty City in various vehicles.
Most of them are equipped with regular radios.
However, there are two sets of vehicles that either don&rsquo;t have radios or override the radio station with some random police chatter.</p>
<p>To complete the game in 100% one has to finish the following side missions<sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup>: Paramedic (reach level 12 in one go), Vigilante (kill 20 criminals on each island), Firefighter (extinguish 20 fires on each island).
They have one thing in common – all require player to spend a substantial amount of time in a vehicle without normal radio stations.</p>
<p>This summer my friends from the <a href="https://discord.gg/qnXy5fY">Polish speedrunning community</a> are going to play the entire GTA 3D trilogy 100% in a relay for <a href="https://www.youtube.com/watch?v=bMmKhBhlW70">a third time</a>.
Previous attempts were completed in <a href="https://www.youtube.com/watch?v=qMldn2NC9N8&amp;list=PLGZ-4E5LK_p21CKN7eCWezKjMtrj0-cf2">55h in 2020</a> and <a href="https://www.youtube.com/watch?v=DSSRpkcpcvs&amp;list=PLGZ-4E5LK_p30SC5z-JJZOU2z2xlaU5Wi&amp;index=1">49h in 2021</a>.
One of them found <a href="https://www.gtagarage.com/mods/show.php?id=23900">this plugin by ThirteenAG</a> that enables radio stations in all vehicles but unfortunately it doesn&rsquo;t work with the Steam version of the game.
It also has a (later discovered to be deliberate) side effect of allowing all vehicles to use the Pay &rsquo;n&rsquo; Spray service (it fixes the car and cancels police wanted level) which changes the core gameplay.
That last thing motivated me to figure out how the plugin worked in order to create a version that: 1. will work with Steam version; 2. will not make the game easier.</p>
<p>For some reason, the original plugin is no longer available on the GTAGarage page.
Luckily, it has been indexed and archived in 2015 by the Wayback Machine so I was able to <a href="https://web.archive.org/web/20150509124202/http://download.gtanet.com/gtagarage/files/32777/III_VC_Radio_for_All_Vehicles.zip?st=R6amzIR5ghQuNaTEIERJCw&amp;e=1431175629">retrieve it for analysis</a>.
It turned out to be a regular DLL file with extension changed to <code>.asi</code>.
GTA III loads <code>.asi</code> files placed inside <code>mss</code> directory in the installation directory on startup – that&rsquo;s great beacause no extra injectors are needed to make mods work.</p>
<p>After a quick analysis it appeared that all the plugin did was:</p>
<ul>
<li>create a thread with delayed execution (never figured out why)</li>
<li>detect the version of the game by checking some magic values in memory</li>
<li>set other magic values in memory depending on the detected verison.
Cool, now I could figure out which offsets in memory did what.</li>
</ul>
<table>
  <thead>
      <tr>
          <th style="text-align: center"><img src="https://blog.andr.host/2022/05/26-iii-radio-ce.png" alt="Cheat Engine values table when running a Steam version of the game." title="Cheat Engine values table when running a Steam version of the game."></th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: center">Cheat Engine values table when running a Steam version of the game.</td>
      </tr>
  </tbody>
</table>
<p>Once I realized which branch should I take for each version of the game I started to play with the values in Cheat Engine debugger.
That&rsquo;s when it turned out that the aforementioned Pay &rsquo;n&rsquo; Spray &ldquo;issue&rdquo; was an intended feature!
At this point I could just recreate the mod (or patch the current one) without this &ldquo;feature&rdquo; but I wanted to fully understand <em>why</em> it worked.
Remember the magic values the mod was setting?
I&rsquo;m not proficient at reverse engineering nor x86 assembly but thanks to <a href="https://queenspalace.dev/">pitpo</a> I managed to figure out that the <code>0xEB</code> in place of <code>0x77</code> was in fact turning a conditional jump into an unconditional jump.</p>
<p>I was sceptical about loading the game binary into Ghidra but I eventually did it only to learn that I could easily map the related code to <code>re3</code>.
Couple years ago a group of fans made an effort to rewrite the original game in order to fix bugs and port the game to new platforms.
It was called <code>re3</code>.
I wrote &ldquo;was&rdquo; because the project has received <a href="https://github.com/github/dmca/blob/master/2021/09/2021-09-29-take-two-legal-action/2021-09-29-take-two-legal-action.md">a takedown from the publisher of GTA, which was later countered, and then Take-Two took it to court</a>…
Thanks to <code>re3</code> I could confirm that the change I&rsquo;m making won&rsquo;t affect any other aspect of the game.</p>
<table>
  <thead>
      <tr>
          <th style="text-align: center"><img src="https://blog.andr.host/2022/05/26-iii-radio-code-comparison.png" alt="Spot 10 differences." title="Spot 10 differences."></th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: center"><code>re3</code> on the left, ghidra output on the right. Spot 10 differences. Whole car list that includes IDs is available <a href="https://gtamods.com/wiki/List_of_vehicles_(III)">here</a>.</td>
      </tr>
  </tbody>
</table>
<p>Now the only thing left was to somehow make it work with a binary shipped by Steam.
That version is supposedly corresponding to the retail version 1.1 however there&rsquo;s also a layer of DRM sprinkled on top.
Once again pitpo&rsquo;s help was invaluable as he pointed me in directrion of <a href="https://github.com/atom0s/Steamless">Steamless</a>.
That tool can take out some variants of the SteamStub DRM.
To my surprise, it just worked.
With the unpacked binary, I was able to trace the required code which turned out to be only slightly offsetted from the 1.1 version.</p>
<p>The final outcome of this journey is <a href="https://github.com/florczakraf/gta-iii-radio-for-all-vehicles">available on my GitHub</a>.
The pun from the post&rsquo;s title is now apparent ;)</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>First there was the 2D universe which included first two games alongside their addons.
In 2001 GTA III was released which gave a start for the 3D universe.
It not only includes Vice City and San Andreas but also handheld games: Advance, Liberty City Stories, Vice City Stories.
The latest universe in the franchise is called HD and it spans across IV (and its two expansions), Chinatown Wars and widely acclaimed V.
GTA worlds blend together when it comes to timelines and appearances of some characters and things like brands.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p>Alongside many other tedious tasks. The whole list is available <a href="https://gta.fandom.com/wiki/100%25_Completion_in_GTA_III">here</a>.&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>]]></content:encoded>
    </item>
    
    <item>
      <title>Livestream Background Generator</title>
      <link>https://blog.andr.host/posts/2021/10/23-stream-background-generator/</link>
      <pubDate>Sat, 23 Oct 2021 00:00:00 +0000</pubDate>
      <author>florczak.raf&#43;blog@gmail.com (Rafał Florczak)</author>
      <guid>https://blog.andr.host/posts/2021/10/23-stream-background-generator/</guid>
      <description>
        <![CDATA[I sometimes stream some silly things on my twitch channel. At 1st of October we had a semi-blind race with friends from the PolishSpeedrunners community in Grand Theft Auto IV. It took around 10 hours for the last person to finish, you can find the summary on the racetime.gg page. GTA speedrunning community has always had a knack for filling the empty screen space creatively.
]]>
      </description>
      
        
          <category>programming</category>
        
      
      <content:encoded><![CDATA[<p>I sometimes stream some silly things on my <a href="https://twitch.tv/andrzejgra">twitch channel</a>.
At 1st of October we had a semi-blind race with friends from the <a href="https://discord.com/invite/qnXy5fY">PolishSpeedrunners community</a> in Grand Theft Auto IV.
It took around 10 hours for the last person to finish, you can find the <a href="https://racetime.gg/gtaiv/travelling-fez-2655">summary on the racetime.gg page</a>.
GTA speedrunning community has always had a knack for filling the empty screen space creatively.</p>
<table>
  <thead>
      <tr>
          <th style="text-align: center"><img src="https://blog.andr.host/2021/10/23-stream-background-generator-gtam-100-trilogy-race.png" alt="2014 GTA 100% Trilogy Race layout." title="2014 GTA 100% Trilogy Race layout."></th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: center">2014 GTA 100% Trilogy Race layout. Source: <a href="https://twitter.com/AdamKuczynski/status/501657547913183232">@AdamKuczynski</a></td>
      </tr>
  </tbody>
</table>
<p>I wanted to pay homage to them so I did my best in the little time I had before the stream.</p>
<table>
  <thead>
      <tr>
          <th style="text-align: center"><img src="https://blog.andr.host/2021/10/23-stream-background-generator-gtaiv-race-layout.png" alt="My layout for GTA IV Blind Race." title="My layout for GTA IV Blind Race."></th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: center">My layout for GTA IV Blind Race)</td>
      </tr>
  </tbody>
</table>
<p>I was satisfied with the result but something was missing.
It lacked the dynamism and after a moment of studying you already knew everything what it had to offer.
So after the race I thought I could make it more unpredictable by increasing the number of images.
And showing them in random order.
And adding random entry animations.</p>
<table>
  <thead>
      <tr>
          <th style="text-align: center"><img src="https://blog.andr.host/2021/10/23-stream-background-generator-generator.gif" alt="Example run of the generator." title="Example run of the generator."></th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: center">Example run of the generator.</td>
      </tr>
  </tbody>
</table>
<p>It took some time and money to get all the assets but the final effect was definitely worth it.
I wrote everything in pure javascript embedded on a static website so I could easily use it as a background in my OBS via Browser Source.
Some of the APIs for drawing work only in Chromium based browsers at the time of writing this.</p>
<p>You can find project&rsquo;s repository on my <a href="https://github.com/florczakraf/layouter">github account</a>.
The <a href="https://andr.host/layouter/layouter.html">live demo</a> is available on my website (the first load will be slow because all the images have to be downloaded).</p>]]></content:encoded>
    </item>
    
    <item>
      <title>Upscaling Maluch Racer 3</title>
      <link>https://blog.andr.host/posts/2020/09/25-upscaling-maluch-racer/</link>
      <pubDate>Fri, 25 Sep 2020 00:00:00 +0000</pubDate>
      <author>florczak.raf&#43;blog@gmail.com (Rafał Florczak)</author>
      <guid>https://blog.andr.host/posts/2020/09/25-upscaling-maluch-racer/</guid>
      <description>
        <![CDATA[I have a lot of free time now since I quit my job at the end of August. A couple days ago I was hanging out on PolishSpeedrunners Discord voice channel and I heard a conversation about an old Polish racing game – Maluch Racer 3 (also known as Bambino Rally 3 and 2Fast Driver 2; you can buy it on Steam). The issue one guy had was related to the fact that he had a 4K monitor and the game UI didn&rsquo;t scale up with the resolution at all. In fact, it looks like it was made for 640x480 or 800x600 at most.
]]>
      </description>
      
        
          <category>games</category>
        
          <category>reverse-engineering</category>
        
      
      <content:encoded><![CDATA[<p>I have a lot of free time now since I quit my job at the end of August.
A couple days ago I was hanging out on <a href="https://discord.com/invite/qnXy5fY">PolishSpeedrunners Discord</a> voice channel and I heard a conversation about an old Polish racing game – Maluch Racer 3 (also known as Bambino Rally 3 and 2Fast Driver 2; you can buy it on <a href="https://store.steampowered.com/app/469550/Bambino_Rally_3/">Steam</a>).
The issue one guy had was related to the fact that he had a 4K monitor and the game UI didn&rsquo;t scale up with the resolution at all.
In fact, it looks like it was made for 640x480 or 800x600 at most.</p>
<table>
  <thead>
      <tr>
          <th style="text-align: center"><img src="https://blog.andr.host/2020/09/25-upscaling-maluch-racer-orig-800x600.png" alt="Screenshot taken at 800x600." title="Screenshot taken at 800x600."></th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: center">Screenshot taken at 800x600. Scene rendering doesn&rsquo;t work properly because I was running the game with Wine on Linux but the UI behaves just like it does on Windows.</td>
      </tr>
  </tbody>
</table>
<p>I first had a look at the files of the game.
It turned out that it stores graphics as DDS and PNG files directly in the game tree without extra intermediate formats or archives.
Just for the sake of it, I upscaled all assets by a factor of 2.
As you might have already guessed, it didn&rsquo;t work.
The game still worked but the UI was the same size as before but only the top-left quarter of each asset was used.
Radar didn&rsquo;t work and all the text was off; the gauge on the speedometer was broken.</p>
<p>That&rsquo;s actually something I anticipated from the very beginning because there ought to be some logic to drawing.
I found references in two types of files: <code>.mis</code> and <code>.dso</code>.
<code>.mis</code> files are just &ldquo;mission&rdquo;, i.e. level descriptions.
Each map in game had a hardcoded parameters for a displayed minimap.
Luckily it was enough to just <code>sed</code> these files with new, twice as big, values.
The only side-effect of the change was a need to generate new shader cache for each level when loading it for the first time but the game takes care of it by itself.</p>
<p><code>.dso</code> files, on the other hand, are a compiled game script files that maintain some internal structures and offsets.
The same trick as before didn&rsquo;t work here and the game crashed when the size of texture changed from 3 to 4 digits.
After doing some research, I found a <a href="https://github.com/GarageGames/Torque3D/blob/7dbfe6994d2706157c1adba22fe02f26455d8687">source code of the newer version of the engine</a> that I could use to write a basic version of parser that was compatible with the game because the basic concepts were still there.
The remaining opcodes and structures were easy to figure out from the trial and error using a hex editor.
Equipped with the parser, I could update all the strings<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup> in the readable form.
The last step was to &ldquo;compile&rdquo; the scripts back to dso files.</p>
<table>
  <thead>
      <tr>
          <th style="text-align: center"><img src="https://blog.andr.host/2020/09/25-upscaling-maluch-racer-1080p-comparison.png" alt="Screenshot taken at 1920x1080 before and after upscaling the UI." title="Screenshot taken at 1920x1080 before and after upscaling the UI."></th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: center">Screenshot taken at 1920x1080 before and after upscaling the UI by a factor of 2. You can only imagine how ridiculously small it is in 4K. Minimap and speedometer are still functional.</td>
      </tr>
  </tbody>
</table>
<p>After some debugging sessions I had everything I needed to create a PoC of an upscaler for the game.
The library I wrote for parsing and patching dso files and the PoC itself (in examples dir) are <a href="https://github.com/florczakraf/dso-tools">available on my github</a>.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>It turned out that texture sizes were also saved as strings. I don&rsquo;t judge.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>]]></content:encoded>
    </item>
    
    <item>
      <title>FUSE &#43; Slack?</title>
      <link>https://blog.andr.host/posts/2020/02/23-fuse/</link>
      <pubDate>Sun, 23 Feb 2020 00:00:00 +0000</pubDate>
      <author>florczak.raf&#43;blog@gmail.com (Rafał Florczak)</author>
      <guid>https://blog.andr.host/posts/2020/02/23-fuse/</guid>
      <description>
        <![CDATA[Some time ago I read a bit about FUSE (Filesystem in Userspace) and I wanted to do some small project to test it out. Since I&rsquo;m using an IRC gateway for chatting on Slack I thought I could craete a simple FUSE driver to let me access our cat images channel with the usual tools just as if they were on my local disk.
]]>
      </description>
      
        
          <category>programming</category>
        
      
      <content:encoded><![CDATA[<p>Some time ago I read a bit about <a href="https://github.com/libfuse/libfuse">FUSE</a> (Filesystem in Userspace) and I wanted to do some small project to test it out.
Since I&rsquo;m using an IRC gateway for chatting on Slack I thought I could craete a simple FUSE driver to let me access our cat images channel with the usual tools just as if they were on my local disk.</p>
<p>FUSE is an interface for unprivileged programs running in userpace to implement virtual filesystems.
Examples of popular and maintained filesystems built on top of FUSE include: <code>sshfs</code> (for mounting remote filesystems over SSH), <code>ntfs-3g</code> (for mounting NTFS partitions), <code>s3fs</code> (for mounting Amazon AWS S3 buckets).</p>
<p>After an evening of prototyping I had a first &ldquo;working&rdquo; version of <code>SlackFS</code>, a virtual filesystem that allowed opening and uploading files from a Slack workspace.
The code is <a href="https://github.com/florczakraf/slackfs">available on my GitHub</a>.
It uses Python FUSE bindings to implement the minimum required set of operations for the whole thing to at least not crash programs that try to access the files.</p>
<p>There are many limitations I&rsquo;m aware of listed in the README.
For example, files from channels are fetched only once, on mount.
Files, however, are fetched lazily when they are accessed for the first time and cached for later use.
Uploading new works but it&rsquo;s not intuitive because I need to change the names of the uploaded files because Slack allows mutliple files with the same name to be sent to a channel.
There are couple problems related to the APIs that I didn&rsquo;t bother resolving because the current version is &ldquo;good enough&rdquo; for a demo and my use case.</p>
<p>To sum up, it was a fun couple evenings.
I ended up with a &ldquo;polished prototype&rdquo;.
Later I gave a short talk at work about FUSE where we live-coded a driver for accessing employees&rsquo; leaves database.
It served two purposes, first one – to show FUSE, second – to show a problem with authorization in the corpo&rsquo;s in-house solution for managing leaves.
Yep, it was possible to access ALL the details (like reasons and types of leaves) for ALL employees in Poland<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>Note from editing in 2022 before publishing: I left the company later that year.
I believe they started to migrate to an outsourced service around that time.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>]]></content:encoded>
    </item>
    
    <item>
      <title>TIL: comm</title>
      <link>https://blog.andr.host/posts/2019/07/16-comm/</link>
      <pubDate>Tue, 16 Jul 2019 00:00:00 +0000</pubDate>
      <author>florczak.raf&#43;blog@gmail.com (Rafał Florczak)</author>
      <guid>https://blog.andr.host/posts/2019/07/16-comm/</guid>
      <description>
        <![CDATA[Sometimes you can find yourself in a situation where you need to compare two sets of data in your shell script. Until now I was unaware that rms is a co-author of the program does just that. The utility is called comm and you can probably find it in your distro&rsquo;s core packages.
]]>
      </description>
      
        
          <category>TIL</category>
        
      
      <content:encoded><![CDATA[<p>Sometimes you can find yourself in a situation where you need to compare two sets of data in your shell script.
Until now I was unaware that <a href="https://stallman.org/">rms</a> is a co-author of the program does just that.
The utility is called <code>comm</code> and you can probably find it in your distro&rsquo;s core packages.</p>
<p>At work I have to track and request matlab licenses for my coworkers in the meta-team of over 100 people that changes quite dynamically lately.
However, since it&rsquo;s a big corpo, there are already some <a href="https://ldap.com/">LDAP</a> groups implemented at the company.
All I had to automate was to fetch and compare the changes in the top-level group that contained all the teams.</p>
<p><code>comm</code> made this super easy once I had the previous and current lists.
Using the <code>-13</code> and <code>-23</code> options I could generate lists of people to add and remove.
The data, however, had to be mailed to managers to get all the necessary approvals and blessings&hellip;
But that was a trivial task since the mail body could be composed using a <code>HEREDOC</code> and piped directly to <code>mail</code> because the server where the cronjob was installed had a proper mail configuration.</p>]]></content:encoded>
    </item>
    
    <item>
      <title>TIL: bubblewrap</title>
      <link>https://blog.andr.host/posts/2019/06/12-bwrap/</link>
      <pubDate>Wed, 12 Jun 2019 00:00:00 +0000</pubDate>
      <author>florczak.raf&#43;blog@gmail.com (Rafał Florczak)</author>
      <guid>https://blog.andr.host/posts/2019/06/12-bwrap/</guid>
      <description>
        <![CDATA[Sometimes we have to use Matlab at work. Whenever that&rsquo;s required, it brings pain for various reasons. My team managed to solve one of them for ourselves and other coworkers. Let&rsquo;s start by describing where the issue comes from.
]]>
      </description>
      
        
          <category>TIL</category>
        
      
      <content:encoded><![CDATA[<p>Sometimes we have to use Matlab at work.
Whenever that&rsquo;s required, it brings pain for various reasons.
My team managed to solve one of them for ourselves and other coworkers.
Let&rsquo;s start by describing where the issue comes from.</p>
<p>Mathworks, the company behind Matlab, provides a decentralized on-prem licensing services to track and authorize use of their products.
Surprise, surprise, it&rsquo;s called <code>Network License Manger</code>.
The only validation it performs is through the shared licensing key and a username taken from the OS running Matlab.
While the corpo-managed Windows don&rsquo;t allow employees to change their usernames, Linux users can use whatever username they want.
Here&rsquo;s the issue: people who decided to use a custom, not-company-assigned username on their computers couldn&rsquo;t use Matlab.</p>
<p>There were couple options to trick Matlab to start.
Starting from the trivial &ldquo;just use the blessed username&rdquo;, through <code>su</code>ing into another user just to execute Matlab, modifying network traffic on the fly, and ending on creating a wrapper that faked the username.
We aimed for the last one.</p>
<p>After some <code>strace</code>ing it turned out that Matlab first fetches the uid of the user that started it and later performs a lookup in <code>/etc/passwd</code> to get the username.
<a href="https://github.com/emdej">emdej</a> suggested that we use <a href="https://github.com/containers/bubblewrap">bubblewrap</a> to fake the username associated with current user.
The wrapping script looked more or less like this:</p>
<ol>
<li>fetch current uid</li>
<li>take the blessed-corpo-username from an env variable or a config file</li>
<li>use the two from above to create a copy of <code>/etc/passwd</code> with an altered entry</li>
<li><code>exec bwrap</code> with a bind for a real <code>/etc/passwd</code> and rest of the filesystem as-is.</li>
</ol>
<p>The lesson learned from this story is that the <code>Network License Manager</code>&rsquo;s authentication sucks (or at least it is configured poorly at our company).
Theoretically we could fake all usernames to a single one as well as the reported hostnames (the combo was used to deduplicate licenses taken from the pool – one user could spin as many instances of matlab on one host as they wanted<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>).</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>We have some matlab scripts that are single-threaded and take a long time to complete. We also have plenty servers that run on double 14-cored Intel Xeons, summing up to 56 logical CPUs&hellip; So it&rsquo;s a neat coincidence.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>]]></content:encoded>
    </item>
    
    <item>
      <title>TIL: fzf</title>
      <link>https://blog.andr.host/posts/2018/08/17-fzf/</link>
      <pubDate>Fri, 17 Aug 2018 00:00:00 +0000</pubDate>
      <author>florczak.raf&#43;blog@gmail.com (Rafał Florczak)</author>
      <guid>https://blog.andr.host/posts/2018/08/17-fzf/</guid>
      <description>
        <![CDATA[I use Bash as my main shell and that&rsquo;s probably not going to change in the forseeable future.1 It&rsquo;s stable and widely used. And all people running their favorite shells still have Bash installed for uncompatible stuff. One of the &lsquo;meh&rsquo; features of Bash is its built-in reverse search.
]]>
      </description>
      
        
          <category>TIL</category>
        
      
      <content:encoded><![CDATA[<p>I use Bash as my main shell and that&rsquo;s probably not going to change in the forseeable future.<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>
It&rsquo;s stable and widely used.
And all people running their favorite shells still have Bash installed for uncompatible stuff.
One of the &lsquo;meh&rsquo; features of Bash is its built-in reverse search.</p>
<p>However, there&rsquo;s a neat tool that I saw my colleague <a href="https://github.com/emdej">emdej</a> use.
Meet <a href="https://github.com/junegunn/fzf">fzf</a>, a command-line fuzzy finder.</p>
<p>Well, it&rsquo;s much more than the tag-line says.
The fuzziness can be used in many contexts and integrated anywhere you want (for example for matching hosts from the ssh config).
fzf comes with default bindings for recursively searching files and commands from history.
The latter, together with:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>HISTSIZE<span style="color:#f92672">=</span>
</span></span><span style="display:flex;"><span>HISTFILESIZE<span style="color:#f92672">=</span>
</span></span><span style="display:flex;"><span>export HISTCONTROL<span style="color:#f92672">=</span>ignoredups:erasedups
</span></span><span style="display:flex;"><span>shopt -s histappend
</span></span><span style="display:flex;"><span>export PROMPT_COMMAND<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>PROMPT_COMMAND:+$PROMPT_COMMAND<span style="color:#e6db74">$&#39;\n&#39;</span><span style="color:#e6db74">}</span><span style="color:#e6db74">history -a; history -c; history -r&#34;</span>
</span></span></code></pre></div><p>for making the history infinite and reloading live<sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup>, makes my life much easier.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>Yep. I&rsquo;m editing this post before publication in 2022 and it seems I was correct. Nothing&rsquo;s changed in my setup in this matter.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p>I often use multiple terminal windows so sharing history (and saving it for later use!) is important for me. Default settings save the history to file on closing the session, with each session overwriting the history file with its own &ldquo;fork&rdquo;.&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>]]></content:encoded>
    </item>
    
    <item>
      <title>TIL: GNU Stow</title>
      <link>https://blog.andr.host/posts/2016/08/02-stow/</link>
      <pubDate>Tue, 02 Aug 2016 00:00:00 +0000</pubDate>
      <author>florczak.raf&#43;blog@gmail.com (Rafał Florczak)</author>
      <guid>https://blog.andr.host/posts/2016/08/02-stow/</guid>
      <description>
        <![CDATA[I started my first job a month ago. Until now I didn&rsquo;t care about managing my configs because I had a single workstation. This has now changed and I wanted to reliably keep track of my preferences for various programs. I thought of keeping them in a git repo for easy version tracking and backup purposes1. The only missing point was a mechanism to use them in a managable way since now they were kept in a separate place.
]]>
      </description>
      
        
          <category>TIL</category>
        
      
      <content:encoded><![CDATA[<p>I started my first job a month ago.
Until now I didn&rsquo;t care about managing my configs because I had a single workstation.
This has now changed and I wanted to reliably keep track of my preferences for various programs.
I thought of keeping them in a git repo for easy version tracking and backup purposes<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>.
The only missing point was a mechanism to use them in a managable way since now they were kept in a separate place.</p>
<p>I found <a href="https://www.gnu.org/software/stow/">GNU <code>Stow</code></a> that fits my needs perfectly.
It allows maintaining symbolic links in a natural matter, just like if you were installing a package.
This way I can just update the repo with configs and there&rsquo;s no need for me to manually keep track of any changes.
The only time I have to do something more is when I add and want to use a config for a new program.</p>
<p>Stowing configs is simple:</p>
<pre tabindex="0"><code>$ stow some_dir/ other_dir/ yet_another_one/
</code></pre><p>By default it installs the files one directory above the current one, hence it&rsquo;s easier to use when you keep the source config directories in a folder directly in your <code>$HOME</code>.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>When I&rsquo;m editing this post before publishing in 2022 I still use the same base I created back then.
It&rsquo;s been revised and made public since then on my <a href="https://github.com/florczakraf/dotfiles">github</a>.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>]]></content:encoded>
    </item>
    
  </channel>
</rss>
