<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://nullonerror.org/feed.xml" rel="self" type="application/atom+xml" /><link href="https://nullonerror.org/" rel="alternate" type="text/html" /><updated>2026-02-13T15:26:58+00:00</updated><id>https://nullonerror.org/feed.xml</id><title type="html">NULL on error</title><subtitle>NULL on error</subtitle><author><name>Rodrigo Delduca</name><email>rodrigo@delduca.org</email></author><entry><title type="html">Hiding information inside images</title><link href="https://nullonerror.org/2026/02/13/hiding-information-inside-images/" rel="alternate" type="text/html" title="Hiding information inside images" /><published>2026-02-13T00:00:00+00:00</published><updated>2026-02-13T00:00:00+00:00</updated><id>https://nullonerror.org/2026/02/13/hiding-information-inside-images</id><content type="html" xml:base="https://nullonerror.org/2026/02/13/hiding-information-inside-images/"><![CDATA[<p>As a child, there was a game called <a href="http://educador.brasilescola.com/estrategias-ensino/mensagem-secreta.htm">secret message</a> which consisted of writing something on a sheet of paper with lemon juice and handing it to a friend who knew that by holding a candle underneath, a message would be revealed — without any of the intermediaries ever knowing.</p>

<p>We can say that this game was a form of <strong>steganography</strong></p>

<blockquote>
  <p>“Steganography (from the Greek ‘hidden writing’) is the study and use of techniques to conceal the existence of a message within another, a form of security through obscurity. In other words, steganography is the particular branch of cryptology that consists of disguising one piece of writing within another in order to mask its true meaning.” - <a href="http://en.wikipedia.org/wiki/Steganography">Wikipedia</a></p>
</blockquote>

<p>Today I’m going to introduce another type of steganography — one that uses images to hide text inside pixels!</p>

<p>An image is composed of pixels, and each pixel represents the smallest possible color point using N bits. The more bits per color, the more colors a pixel can represent.</p>

<p>In my example I will focus only on <em>32-bit</em> images (<em>RGBA</em> or <em>ARGB</em>) where <em>8 bits</em> are used per color component. Obviously the technique is easily adaptable to other formats.</p>

<p><img src="/public/2015-04-05-escondendo-informacoes-dentro-de-imagens/rgba.png" alt="image" title="https://commons.wikimedia.org/wiki/File:HexRGBAbits.png" /></p>

<p>What happens if I change a single bit of the red component of every pixel? Even with the original image side by side, we would be unable to notice the difference — only a <em>hashing</em> algorithm like SHA1 could tell the files apart.</p>

<p>Therefore, I can use this same trick of altering pixels to hide a message inside the image, and only someone who knows the implementation will know how to extract the message.</p>

<p>But to do this we need to separate each bit of the message and change only one or two bits of each color component. It sounds like very little space, but if we do the math, an 800×600 image — which is considered low resolution today — can hold</p>

<blockquote>
  <p>800 width _ 600 height = 480000 pixels<br />
480000 pixels _ 4 components = 1920000 bits<br />
1920000 bits / 8 = 240000 bytes</p>
</blockquote>

<p>In other words, we can fit a message of up to ~30 KB in an 800×600 image :)</p>

<p>Remember, we are talking about pixels, not file size — that depends on the chosen format. And speaking of format, this only works with <a href="http://en.wikipedia.org/wiki/Lossless_compression#Graphics">lossless</a> formats.</p>

<h2 id="implementation">Implementation</h2>

<p>To achieve our goal we need to master the <strong>ancient art of bit twiddling</strong>. There is an excellent resource on the subject called: <a href="https://graphics.stanford.edu/~seander/bithacks.html">Bit Twiddling Hacks</a></p>

<p>Writing a hidden message:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">bool</span> <span class="nf">write</span><span class="p">(</span><span class="k">const</span> <span class="n">QString</span><span class="o">&amp;</span> <span class="n">in</span><span class="p">,</span> <span class="k">const</span> <span class="n">QString</span><span class="o">&amp;</span> <span class="n">out</span><span class="p">,</span> <span class="k">const</span> <span class="n">QString</span><span class="o">&amp;</span> <span class="n">text</span><span class="p">){</span>
  <span class="n">QImage</span> <span class="n">image</span><span class="p">;</span>
  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">image</span><span class="p">.</span><span class="n">load</span><span class="p">(</span><span class="n">in</span><span class="p">))</span>
      <span class="k">return</span> <span class="nb">false</span><span class="p">;</span>

  <span class="n">QImage</span> <span class="n">result</span> <span class="o">=</span> <span class="n">image</span><span class="p">.</span><span class="n">copy</span><span class="p">();</span>
  <span class="kt">int</span> <span class="n">size</span> <span class="o">=</span> <span class="n">text</span><span class="p">.</span><span class="n">size</span><span class="p">();</span>

  <span class="n">QByteArray</span> <span class="n">bytes</span><span class="p">;</span>
  <span class="n">bytes</span><span class="p">.</span><span class="n">reserve</span><span class="p">(</span><span class="n">size</span> <span class="o">+</span> <span class="n">headerSize</span><span class="p">);</span>
  <span class="n">bytes</span> <span class="o">+=</span> <span class="n">QString</span><span class="p">(</span><span class="s">"%1"</span><span class="p">).</span><span class="n">arg</span><span class="p">(</span><span class="n">size</span><span class="p">,</span> <span class="n">headerSize</span><span class="p">,</span> <span class="mi">10</span><span class="p">,</span> <span class="n">QChar</span><span class="p">(</span><span class="sc">'0'</span><span class="p">));</span>
  <span class="n">bytes</span> <span class="o">+=</span> <span class="n">text</span><span class="p">.</span><span class="n">toLocal8Bit</span><span class="p">();</span>

  <span class="n">QBitArray</span> <span class="n">bits</span> <span class="o">=</span> <span class="n">byteArrayToBitarray</span><span class="p">(</span><span class="n">bytes</span><span class="p">);</span>

  <span class="c1">// Iterate over each pixel in the image</span>
  <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">index</span> <span class="o">=</span> <span class="mi">0</span><span class="p">,</span> <span class="n">y</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">y</span> <span class="o">&lt;</span> <span class="n">image</span><span class="p">.</span><span class="n">height</span><span class="p">();</span> <span class="o">++</span><span class="n">y</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">x</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">x</span> <span class="o">&lt;</span> <span class="n">image</span><span class="p">.</span><span class="n">width</span><span class="p">();</span> <span class="o">++</span><span class="n">x</span><span class="p">)</span> <span class="p">{</span>
      <span class="k">if</span> <span class="p">(</span><span class="n">index</span> <span class="o">&gt;=</span> <span class="n">bits</span><span class="p">.</span><span class="n">count</span><span class="p">())</span>
        <span class="k">break</span><span class="p">;</span>

      <span class="c1">// Extract each color component individually</span>
      <span class="n">QRgb</span> <span class="n">pixel</span> <span class="o">=</span> <span class="n">image</span><span class="p">.</span><span class="n">pixel</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">);</span>
      <span class="kt">int</span> <span class="n">red</span>   <span class="o">=</span> <span class="n">qRed</span><span class="p">(</span><span class="n">pixel</span><span class="p">);</span>
      <span class="kt">int</span> <span class="n">green</span> <span class="o">=</span> <span class="n">qGreen</span><span class="p">(</span><span class="n">pixel</span><span class="p">);</span>
      <span class="kt">int</span> <span class="n">blue</span>  <span class="o">=</span> <span class="n">qBlue</span><span class="p">(</span><span class="n">pixel</span><span class="p">);</span>
      <span class="kt">int</span> <span class="n">alpha</span> <span class="o">=</span> <span class="n">qAlpha</span><span class="p">(</span><span class="n">pixel</span><span class="p">);</span>

      <span class="c1">// For each component, take one bit from the message</span>
      <span class="c1">// and turn the last bit of the component on or off</span>
      <span class="c1">// to turn on:  component | (1 &lt;&lt; 0x00)</span>
      <span class="c1">// to turn off: component &amp; ~(1 &lt;&lt; 0x00)</span>
      <span class="n">red</span>   <span class="o">=</span> <span class="n">bits</span><span class="p">[</span><span class="n">index</span> <span class="o">+</span> <span class="mi">0</span><span class="p">]</span> <span class="o">?</span> <span class="n">red</span>   <span class="o">|</span> <span class="p">(</span><span class="mi">1</span> <span class="o">&lt;&lt;</span> <span class="mh">0x00</span><span class="p">)</span> <span class="o">:</span> <span class="n">red</span>   <span class="o">&amp;</span> <span class="o">~</span><span class="p">(</span><span class="mi">1</span> <span class="o">&lt;&lt;</span> <span class="mh">0x00</span><span class="p">);</span>
      <span class="n">green</span> <span class="o">=</span> <span class="n">bits</span><span class="p">[</span><span class="n">index</span> <span class="o">+</span> <span class="mi">1</span><span class="p">]</span> <span class="o">?</span> <span class="n">green</span> <span class="o">|</span> <span class="p">(</span><span class="mi">1</span> <span class="o">&lt;&lt;</span> <span class="mh">0x00</span><span class="p">)</span> <span class="o">:</span> <span class="n">green</span> <span class="o">&amp;</span> <span class="o">~</span><span class="p">(</span><span class="mi">1</span> <span class="o">&lt;&lt;</span> <span class="mh">0x00</span><span class="p">);</span>
      <span class="n">blue</span>  <span class="o">=</span> <span class="n">bits</span><span class="p">[</span><span class="n">index</span> <span class="o">+</span> <span class="mi">2</span><span class="p">]</span> <span class="o">?</span> <span class="n">blue</span>  <span class="o">|</span> <span class="p">(</span><span class="mi">1</span> <span class="o">&lt;&lt;</span> <span class="mh">0x00</span><span class="p">)</span> <span class="o">:</span> <span class="n">blue</span>  <span class="o">&amp;</span> <span class="o">~</span><span class="p">(</span><span class="mi">1</span> <span class="o">&lt;&lt;</span> <span class="mh">0x00</span><span class="p">);</span>
      <span class="n">alpha</span> <span class="o">=</span> <span class="n">bits</span><span class="p">[</span><span class="n">index</span> <span class="o">+</span> <span class="mi">3</span><span class="p">]</span> <span class="o">?</span> <span class="n">alpha</span> <span class="o">|</span> <span class="p">(</span><span class="mi">1</span> <span class="o">&lt;&lt;</span> <span class="mh">0x00</span><span class="p">)</span> <span class="o">:</span> <span class="n">alpha</span> <span class="o">&amp;</span> <span class="o">~</span><span class="p">(</span><span class="mi">1</span> <span class="o">&lt;&lt;</span> <span class="mh">0x00</span><span class="p">);</span>

      <span class="c1">// Same thing for the second-to-last bit</span>
      <span class="n">red</span>   <span class="o">=</span> <span class="n">bits</span><span class="p">[</span><span class="n">index</span> <span class="o">+</span> <span class="mi">4</span><span class="p">]</span> <span class="o">?</span> <span class="n">red</span>   <span class="o">|</span> <span class="p">(</span><span class="mi">1</span> <span class="o">&lt;&lt;</span> <span class="mh">0x01</span><span class="p">)</span> <span class="o">:</span> <span class="n">red</span>   <span class="o">&amp;</span> <span class="o">~</span><span class="p">(</span><span class="mi">1</span> <span class="o">&lt;&lt;</span> <span class="mh">0x01</span><span class="p">);</span>
      <span class="n">green</span> <span class="o">=</span> <span class="n">bits</span><span class="p">[</span><span class="n">index</span> <span class="o">+</span> <span class="mi">5</span><span class="p">]</span> <span class="o">?</span> <span class="n">green</span> <span class="o">|</span> <span class="p">(</span><span class="mi">1</span> <span class="o">&lt;&lt;</span> <span class="mh">0x01</span><span class="p">)</span> <span class="o">:</span> <span class="n">green</span> <span class="o">&amp;</span> <span class="o">~</span><span class="p">(</span><span class="mi">1</span> <span class="o">&lt;&lt;</span> <span class="mh">0x01</span><span class="p">);</span>
      <span class="n">blue</span>  <span class="o">=</span> <span class="n">bits</span><span class="p">[</span><span class="n">index</span> <span class="o">+</span> <span class="mi">6</span><span class="p">]</span> <span class="o">?</span> <span class="n">blue</span>  <span class="o">|</span> <span class="p">(</span><span class="mi">1</span> <span class="o">&lt;&lt;</span> <span class="mh">0x01</span><span class="p">)</span> <span class="o">:</span> <span class="n">blue</span>  <span class="o">&amp;</span> <span class="o">~</span><span class="p">(</span><span class="mi">1</span> <span class="o">&lt;&lt;</span> <span class="mh">0x01</span><span class="p">);</span>
      <span class="n">alpha</span> <span class="o">=</span> <span class="n">bits</span><span class="p">[</span><span class="n">index</span> <span class="o">+</span> <span class="mi">7</span><span class="p">]</span> <span class="o">?</span> <span class="n">alpha</span> <span class="o">|</span> <span class="p">(</span><span class="mi">1</span> <span class="o">&lt;&lt;</span> <span class="mh">0x01</span><span class="p">)</span> <span class="o">:</span> <span class="n">alpha</span> <span class="o">&amp;</span> <span class="o">~</span><span class="p">(</span><span class="mi">1</span> <span class="o">&lt;&lt;</span> <span class="mh">0x01</span><span class="p">);</span>

      <span class="c1">// Write the pixel back to its original position</span>
      <span class="n">result</span><span class="p">.</span><span class="n">setPixel</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">,</span> <span class="n">qRgba</span><span class="p">(</span><span class="n">red</span><span class="p">,</span> <span class="n">green</span><span class="p">,</span> <span class="n">blue</span><span class="p">,</span> <span class="n">alpha</span><span class="p">));</span>

      <span class="n">index</span> <span class="o">+=</span> <span class="mi">8</span><span class="p">;</span>
    <span class="p">}</span>
  <span class="p">}</span>

  <span class="k">return</span> <span class="n">result</span><span class="p">.</span><span class="n">save</span><span class="p">(</span><span class="n">out</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Reading a hidden message from an image:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">QByteArray</span> <span class="nf">read</span><span class="p">(</span><span class="k">const</span> <span class="n">QString</span><span class="o">&amp;</span> <span class="n">filename</span><span class="p">)</span> <span class="p">{</span>
  <span class="n">QBitArray</span> <span class="n">bits</span><span class="p">(</span><span class="mi">8</span><span class="p">);</span>
  <span class="n">QByteArray</span> <span class="n">bytes</span><span class="p">;</span>
  <span class="n">bytes</span><span class="p">.</span><span class="n">reserve</span><span class="p">(</span><span class="n">headerSize</span><span class="p">);</span>
  <span class="kt">int</span> <span class="n">bytesToRead</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>

  <span class="n">QImage</span> <span class="n">image</span><span class="p">;</span>
  <span class="k">if</span> <span class="p">(</span><span class="n">image</span><span class="p">.</span><span class="n">load</span><span class="p">(</span><span class="n">filename</span><span class="p">))</span> <span class="p">{</span>
    <span class="c1">// Iterate over each pixel in the image</span>
    <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">y</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">y</span> <span class="o">&lt;</span> <span class="n">image</span><span class="p">.</span><span class="n">height</span><span class="p">();</span> <span class="o">++</span><span class="n">y</span><span class="p">)</span> <span class="p">{</span>
      <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">x</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">x</span> <span class="o">&lt;</span> <span class="n">image</span><span class="p">.</span><span class="n">width</span><span class="p">();</span> <span class="o">++</span><span class="n">x</span><span class="p">)</span> <span class="p">{</span>
        <span class="kt">uint32_t</span> <span class="n">index</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
        <span class="n">QRgb</span> <span class="n">pixel</span> <span class="o">=</span> <span class="n">image</span><span class="p">.</span><span class="n">pixel</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">);</span>

        <span class="c1">// For each pixel, extract the last and second-to-last bit</span>
        <span class="c1">// of each color component</span>
        <span class="n">bits</span><span class="p">[</span><span class="n">index</span><span class="o">++</span><span class="p">]</span> <span class="o">=</span> <span class="n">qRed</span><span class="p">(</span><span class="n">pixel</span><span class="p">)</span>   <span class="o">&amp;</span> <span class="mi">1</span> <span class="o">&lt;&lt;</span> <span class="mh">0x00</span><span class="p">;</span>
        <span class="n">bits</span><span class="p">[</span><span class="n">index</span><span class="o">++</span><span class="p">]</span> <span class="o">=</span> <span class="n">qGreen</span><span class="p">(</span><span class="n">pixel</span><span class="p">)</span> <span class="o">&amp;</span> <span class="mi">1</span> <span class="o">&lt;&lt;</span> <span class="mh">0x00</span><span class="p">;</span>
        <span class="n">bits</span><span class="p">[</span><span class="n">index</span><span class="o">++</span><span class="p">]</span> <span class="o">=</span> <span class="n">qBlue</span><span class="p">(</span><span class="n">pixel</span><span class="p">)</span>  <span class="o">&amp;</span> <span class="mi">1</span> <span class="o">&lt;&lt;</span> <span class="mh">0x00</span><span class="p">;</span>
        <span class="n">bits</span><span class="p">[</span><span class="n">index</span><span class="o">++</span><span class="p">]</span> <span class="o">=</span> <span class="n">qAlpha</span><span class="p">(</span><span class="n">pixel</span><span class="p">)</span> <span class="o">&amp;</span> <span class="mi">1</span> <span class="o">&lt;&lt;</span> <span class="mh">0x00</span><span class="p">;</span>

        <span class="n">bits</span><span class="p">[</span><span class="n">index</span><span class="o">++</span><span class="p">]</span> <span class="o">=</span> <span class="n">qRed</span><span class="p">(</span><span class="n">pixel</span><span class="p">)</span>   <span class="o">&amp;</span> <span class="mi">1</span> <span class="o">&lt;&lt;</span> <span class="mh">0x01</span><span class="p">;</span>
        <span class="n">bits</span><span class="p">[</span><span class="n">index</span><span class="o">++</span><span class="p">]</span> <span class="o">=</span> <span class="n">qGreen</span><span class="p">(</span><span class="n">pixel</span><span class="p">)</span> <span class="o">&amp;</span> <span class="mi">1</span> <span class="o">&lt;&lt;</span> <span class="mh">0x01</span><span class="p">;</span>
        <span class="n">bits</span><span class="p">[</span><span class="n">index</span><span class="o">++</span><span class="p">]</span> <span class="o">=</span> <span class="n">qBlue</span><span class="p">(</span><span class="n">pixel</span><span class="p">)</span>  <span class="o">&amp;</span> <span class="mi">1</span> <span class="o">&lt;&lt;</span> <span class="mh">0x01</span><span class="p">;</span>
        <span class="n">bits</span><span class="p">[</span><span class="n">index</span><span class="o">++</span><span class="p">]</span> <span class="o">=</span> <span class="n">qAlpha</span><span class="p">(</span><span class="n">pixel</span><span class="p">)</span> <span class="o">&amp;</span> <span class="mi">1</span> <span class="o">&lt;&lt;</span> <span class="mh">0x01</span><span class="p">;</span>

        <span class="c1">// Convert the 8 bits into 1 byte and append to the bytes list</span>
        <span class="n">bytes</span> <span class="o">+=</span> <span class="n">bitArrayToByteArray</span><span class="p">(</span><span class="n">bits</span><span class="p">);</span>

        <span class="c1">// We need to know how many bytes to read;</span>
        <span class="c1">// for that, a header with this information</span>
        <span class="c1">// is inserted at the very beginning of the write process</span>
        <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">bytesToRead</span> <span class="o">&amp;&amp;</span> <span class="n">bytes</span><span class="p">.</span><span class="n">size</span><span class="p">()</span> <span class="o">==</span> <span class="n">headerSize</span><span class="p">)</span> <span class="p">{</span>
          <span class="kt">bool</span> <span class="n">ok</span><span class="p">;</span>
          <span class="n">bytesToRead</span> <span class="o">=</span> <span class="n">bytes</span><span class="p">.</span><span class="n">toInt</span><span class="p">(</span><span class="o">&amp;</span><span class="n">ok</span><span class="p">);</span>
          <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">ok</span><span class="p">)</span>
            <span class="k">return</span> <span class="n">bytes</span><span class="p">;</span>

          <span class="n">bytes</span><span class="p">.</span><span class="n">clear</span><span class="p">();</span>
          <span class="n">bytes</span><span class="p">.</span><span class="n">reserve</span><span class="p">(</span><span class="n">bytesToRead</span><span class="p">);</span>
        <span class="p">}</span>

        <span class="c1">// Read everything there was to read — return the bytes</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">bytes</span><span class="p">.</span><span class="n">size</span><span class="p">()</span> <span class="o">==</span> <span class="n">bytesToRead</span><span class="p">)</span> <span class="p">{</span>
          <span class="k">return</span> <span class="n">bytes</span><span class="p">;</span>
        <span class="p">}</span>
      <span class="p">}</span>
    <span class="p">}</span>
  <span class="p">}</span>

  <span class="k">return</span> <span class="n">bytes</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="proof">Proof</h2>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ file gioconda.png
gioconda.png: PNG image data, 404 x 410, 8-bit/color RGBA, non-interlaced
$ ./stenog -i gioconda.png -o gioconda_stenog.png -m "flipping bits whilst updating pixels"
$ file gioconda_stenog.png
gioconda_stenog.png: PNG image data, 404 x 410, 8-bit/color RGBA, non-interlaced
$ ls -lah *.png
-rw-r--r--@ 1 Skhaz  staff   378K Apr  3 17:33 gioconda.png
-rw-r--r--  1 Skhaz  staff   394K Apr  3 17:46 gioconda_stenog.png
$ ./stenog -i gioconda_stenog.png
flipping bits whilst updating pixels
</code></pre></div></div>

<p>The file size changed slightly due to the format used — in this case, PNG.</p>

<p><em>This technique can be used to hide not only text, but also other images, sounds, or any kind of data.</em></p>

<h2 id="talk-is-cheap-show-me-the-code">Talk is cheap. Show me the code</h2>

<p>The source code can be found in this <a href="https://gist.github.com/skhaz/4e83e245f41560634be4">gist</a></p>

<p><img src="/public/2015-04-05-escondendo-informacoes-dentro-de-imagens/gioconda_stenog.png" alt="decipher-me-or-I-shall-devour-you" class="center" /></p>]]></content><author><name>Rodrigo Delduca</name><email>rodrigo@delduca.org</email></author><summary type="html"><![CDATA[As a child, there was a game called secret message which consisted of writing something on a sheet of paper with lemon juice and handing it to a friend who knew that by holding a candle underneath, a message would be revealed — without any of the intermediaries ever knowing.]]></summary></entry><entry><title type="html">Carimbo now have a better stack trace and Sentry integration</title><link href="https://nullonerror.org/2025/09/11/carimbo-now-have-a-better-stack-trace-and-sentry-integration/" rel="alternate" type="text/html" title="Carimbo now have a better stack trace and Sentry integration" /><published>2025-09-11T00:00:00+00:00</published><updated>2025-09-11T00:00:00+00:00</updated><id>https://nullonerror.org/2025/09/11/carimbo-now-have-a-better-stack-trace-and-sentry-integration</id><content type="html" xml:base="https://nullonerror.org/2025/09/11/carimbo-now-have-a-better-stack-trace-and-sentry-integration/"><![CDATA[<p>As I’ve already said countless times, I’m working on my first game for Steam, which you can check out online at <a href="https://reprobate.site/">reprobate.site</a>.</p>

<p>Since it’s a paid game, I need to provide proper bug support. With that in mind, and based on both professional and personal experience, I decided to use <a href="https://sentry.io/">Sentry</a>.</p>

<p>At first, integrating C++ and Lua should have been straightforward, but I ran into some issues when using the Conan package manager.</p>

<p>Initially, the package wasn’t being included in the compiler’s include flags, which led me to open an issue both on <a href="https://conan.io/center">Conan Center</a> and on <a href="https://conan.io/center/recipes/sentry-native">Sentry Native</a>.</p>

<p>After spending a whole day on this, I eventually found out that the fix was actually pretty simple. Now Carimbo has native support for Sentry, both on the web (WebAssembly) and natively (Android, iOS, Linux, Windows, and macOS).</p>

<p>Here’s how I managed to get it working.</p>

<h3 id="cmake--conan">CMake &amp; Conan</h3>

<p>This was certainly my biggest problem. For some reason, even when following Conan’s documentation, I couldn’t get it to include the header path for Sentry Native. In the end, my solution looked like this:</p>

<div class="language-cmake highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">find_package</span><span class="p">(</span>sentry CONFIG QUIET<span class="p">)</span>

...

<span class="nb">if</span><span class="p">(</span>sentry_FOUND<span class="p">)</span>
  <span class="nb">target_link_libraries</span><span class="p">(</span><span class="si">${</span><span class="nv">PROJECT_NAME</span><span class="si">}</span> PRIVATE sentry-native::sentry-native<span class="p">)</span>
  <span class="nb">target_compile_definitions</span><span class="p">(</span><span class="si">${</span><span class="nv">PROJECT_NAME</span><span class="si">}</span> PRIVATE HAVE_SENTRY=1<span class="p">)</span>
<span class="nb">endif</span><span class="p">()</span>
</code></pre></div></div>

<h3 id="web--native-at-same-time">Web &amp; Native at same time</h3>

<p>The native part was pretty easy, but for the web part I had an insight while walking, because I realized I could inject JavaScript using the Emscripten API.</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">enginefactory</span><span class="o">&amp;</span> <span class="n">enginefactory</span><span class="o">::</span><span class="n">with_sentry</span><span class="p">(</span><span class="k">const</span> <span class="n">std</span><span class="o">::</span><span class="n">string</span><span class="o">&amp;</span> <span class="n">dsn</span><span class="p">)</span> <span class="k">noexcept</span> <span class="p">{</span>
  <span class="k">if</span> <span class="p">(</span><span class="n">dsn</span><span class="p">.</span><span class="n">empty</span><span class="p">())</span> <span class="p">{</span>
    <span class="k">return</span> <span class="o">*</span><span class="k">this</span><span class="p">;</span>
  <span class="p">}</span>

  <span class="cp">#ifdef EMSCRIPTEN
</span>    <span class="k">const</span> <span class="k">auto</span> <span class="n">script</span> <span class="o">=</span> <span class="n">std</span><span class="o">::</span><span class="n">format</span><span class="p">(</span>
      <span class="s">R"javascript(
        (function(){{
          const __dsn="{}";
          if (window.Sentry &amp;&amp; window.__sentry_inited__) return;
          const script = document.createElement('script');
          script.src = 'https://cdn.jsdelivr.net/npm/@sentry/browser@latest/build/bundle.min.js';
          script.crossOrigin = 'anonymous';
          script.defer = true;
          script.onload = function(){{
            if (!window.Sentry) return;
            window.Sentry.init({{ dsn: __dsn }});
            window.__sentry_inited__ = true;
          }};
          document.head.appendChild(script);
        }})();
      )javascript"</span><span class="p">,</span>
      <span class="n">dsn</span>
    <span class="p">);</span>

    <span class="n">emscripten_run_script</span><span class="p">(</span><span class="n">script</span><span class="p">.</span><span class="n">c_str</span><span class="p">());</span>
  <span class="cp">#endif
</span>
  <span class="cp">#if defined(HAVE_SENTRY) &amp;&amp; !defined(SANDBOX)
</span>    <span class="k">auto</span> <span class="o">*</span><span class="n">options</span> <span class="o">=</span> <span class="n">sentry_options_new</span><span class="p">();</span>
    <span class="n">sentry_options_set_dsn</span><span class="p">(</span><span class="n">options</span><span class="p">,</span> <span class="n">dsn</span><span class="p">.</span><span class="n">c_str</span><span class="p">());</span>
    <span class="c1">// sentry_options_set_debug(options, 1);</span>
    <span class="c1">// sentry_options_set_logger_level(options, SENTRY_LEVEL_DEBUG);</span>
    <span class="n">sentry_init</span><span class="p">(</span><span class="n">options</span><span class="p">);</span>
  <span class="cp">#endif
</span>
  <span class="k">return</span> <span class="o">*</span><span class="k">this</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="better-lua-stack-traces">Better Lua Stack Traces</h3>

<p>Since the game assets are stored in a compressed file, PhysicsFS provides an API to handle them transparently. It’s great for distributing the game — you only need the cartridge.zip and the executable — and it works even better on the web. The engine must provide a searcher so that Lua can find the game’s other Lua scripts. For this, I use a custom searcher: it first looks for the scripts inside the game package, and if they’re not found, it falls back to the interpreter’s default search to load them from the standard library.</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">sol</span><span class="o">::</span><span class="n">object</span> <span class="nf">searcher</span><span class="p">(</span><span class="n">sol</span><span class="o">::</span><span class="n">this_state</span> <span class="n">state</span><span class="p">,</span> <span class="k">const</span> <span class="n">std</span><span class="o">::</span><span class="n">string</span><span class="o">&amp;</span> <span class="n">module</span><span class="p">)</span> <span class="p">{</span>
  <span class="n">sol</span><span class="o">::</span><span class="n">state_view</span> <span class="n">lua</span><span class="p">{</span><span class="n">state</span><span class="p">};</span>

  <span class="k">const</span> <span class="k">auto</span> <span class="n">filename</span> <span class="o">=</span> <span class="n">std</span><span class="o">::</span><span class="n">format</span><span class="p">(</span><span class="s">"scripts/{}.lua"</span><span class="p">,</span> <span class="n">module</span><span class="p">);</span>
  <span class="k">const</span> <span class="k">auto</span> <span class="n">buffer</span> <span class="o">=</span> <span class="n">storage</span><span class="o">::</span><span class="n">io</span><span class="o">::</span><span class="n">read</span><span class="p">(</span><span class="n">filename</span><span class="p">);</span>
  <span class="n">std</span><span class="o">::</span><span class="n">string_view</span> <span class="n">script</span><span class="p">{</span><span class="k">reinterpret_cast</span><span class="o">&lt;</span><span class="k">const</span> <span class="kt">char</span> <span class="o">*&gt;</span><span class="p">(</span><span class="n">buffer</span><span class="p">.</span><span class="n">data</span><span class="p">()),</span> <span class="n">buffer</span><span class="p">.</span><span class="n">size</span><span class="p">()};</span>

  <span class="k">const</span> <span class="k">auto</span> <span class="n">loader</span> <span class="o">=</span> <span class="n">lua</span><span class="p">.</span><span class="n">load</span><span class="p">(</span><span class="n">script</span><span class="p">,</span> <span class="n">std</span><span class="o">::</span><span class="n">format</span><span class="p">(</span><span class="s">"@{}"</span><span class="p">,</span> <span class="n">filename</span><span class="p">));</span>
  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">loader</span><span class="p">.</span><span class="n">valid</span><span class="p">())</span> <span class="p">[[</span><span class="n">unlikely</span><span class="p">]]</span> <span class="p">{</span>
    <span class="n">sol</span><span class="o">::</span><span class="n">error</span> <span class="n">err</span> <span class="o">=</span> <span class="n">loader</span><span class="p">;</span>
    <span class="k">throw</span> <span class="n">std</span><span class="o">::</span><span class="n">runtime_error</span><span class="p">(</span><span class="n">err</span><span class="p">.</span><span class="n">what</span><span class="p">());</span>
  <span class="p">}</span>

  <span class="k">return</span> <span class="n">sol</span><span class="o">::</span><span class="n">make_object</span><span class="p">(</span><span class="n">lua</span><span class="p">,</span> <span class="n">loader</span><span class="p">.</span><span class="n">get</span><span class="o">&lt;</span><span class="n">sol</span><span class="o">::</span><span class="n">protected_function</span><span class="o">&gt;</span><span class="p">());</span>
<span class="p">}</span>

<span class="n">lua</span><span class="p">[</span><span class="s">"searcher"</span><span class="p">]</span> <span class="o">=</span> <span class="o">&amp;</span><span class="n">searcher</span><span class="p">;</span>

<span class="k">const</span> <span class="k">auto</span> <span class="n">inject</span> <span class="o">=</span> <span class="n">std</span><span class="o">::</span><span class="n">format</span><span class="p">(</span><span class="s">R"lua(
  local list = package.searchers or package.loaders
  table.insert(list, searcher)
)lua"</span><span class="p">);</span>

<span class="n">lua</span><span class="p">.</span><span class="n">script</span><span class="p">(</span><span class="n">inject</span><span class="p">);</span>
</code></pre></div></div>

<p>To improve stack traces, the secret lies in the second parameter of lua.load. You can pass a string starting with <code class="language-plaintext highlighter-rouge">@</code> followed by the file name.</p>

<p>This alone gives you a much richer stack trace.</p>

<h3 id="error-handling">Error handling</h3>

<p>In Carimbo, I have a terminate hook that catches any exception and atexit hooks to always handle cleanup.</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[[</span><span class="n">noreturn</span><span class="p">]]</span> <span class="kt">void</span> <span class="nf">fail</span><span class="p">()</span> <span class="p">{</span>
  <span class="k">if</span> <span class="p">(</span><span class="k">const</span> <span class="k">auto</span> <span class="n">ptr</span> <span class="o">=</span> <span class="n">std</span><span class="o">::</span><span class="n">current_exception</span><span class="p">())</span> <span class="p">{</span>
    <span class="k">const</span> <span class="kt">char</span><span class="o">*</span> <span class="n">error</span> <span class="o">=</span> <span class="nb">nullptr</span><span class="p">;</span>

    <span class="k">try</span> <span class="p">{</span>
      <span class="n">std</span><span class="o">::</span><span class="n">rethrow_exception</span><span class="p">(</span><span class="n">ptr</span><span class="p">);</span>
    <span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="k">const</span> <span class="n">std</span><span class="o">::</span><span class="n">bad_exception</span><span class="o">&amp;</span><span class="p">)</span> <span class="p">{</span>
    <span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="k">const</span> <span class="n">std</span><span class="o">::</span><span class="n">exception</span><span class="o">&amp;</span> <span class="n">e</span><span class="p">)</span> <span class="p">{</span>
      <span class="n">error</span> <span class="o">=</span> <span class="n">e</span><span class="p">.</span><span class="n">what</span><span class="p">();</span>
    <span class="p">}</span> <span class="k">catch</span> <span class="p">(...)</span> <span class="p">{</span>
      <span class="n">error</span> <span class="o">=</span> <span class="s">"Unhandled unknown exception"</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="k">if</span> <span class="p">(</span><span class="n">error</span><span class="p">)</span> <span class="p">{</span>
      <span class="cp">#ifdef HAVE_SENTRY
</span>        <span class="k">const</span> <span class="k">auto</span> <span class="n">exc</span> <span class="o">=</span> <span class="n">sentry_value_new_exception</span><span class="p">(</span><span class="s">"exception"</span><span class="p">,</span> <span class="n">error</span><span class="p">);</span>
        <span class="k">const</span> <span class="k">auto</span> <span class="n">ev</span> <span class="o">=</span> <span class="n">sentry_value_new_event</span><span class="p">();</span>

        <span class="n">sentry_event_add_exception</span><span class="p">(</span><span class="n">ev</span><span class="p">,</span> <span class="n">exc</span><span class="p">);</span>

        <span class="n">sentry_capture_event</span><span class="p">(</span><span class="n">ev</span><span class="p">);</span>
      <span class="cp">#endif
</span>
      <span class="cp">#ifdef HAVE_STACKTRACE
</span>        <span class="n">boost</span><span class="o">::</span><span class="n">stacktrace</span><span class="o">::</span><span class="n">stacktrace</span> <span class="n">st</span><span class="p">;</span>
        <span class="n">std</span><span class="o">::</span><span class="n">println</span><span class="p">(</span><span class="n">stderr</span><span class="p">,</span> <span class="s">"Stack trace:</span><span class="se">\n</span><span class="s">{}</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">boost</span><span class="o">::</span><span class="n">stacktrace</span><span class="o">::</span><span class="n">to_string</span><span class="p">(</span><span class="n">st</span><span class="p">));</span>
      <span class="cp">#endif
</span>
      <span class="n">std</span><span class="o">::</span><span class="n">println</span><span class="p">(</span><span class="n">stderr</span><span class="p">,</span> <span class="s">"{}"</span><span class="p">,</span> <span class="n">error</span><span class="p">);</span>

      <span class="n">SDL_ShowSimpleMessageBox</span><span class="p">(</span><span class="n">SDL_MESSAGEBOX_ERROR</span><span class="p">,</span> <span class="s">"Ink Spill Disaster"</span><span class="p">,</span> <span class="n">error</span><span class="p">,</span> <span class="nb">nullptr</span><span class="p">);</span>

      <span class="cp">#ifdef DEBUG
</span>        <span class="cp">#ifdef _MSC_VER
</span>          <span class="n">__debugbreak</span><span class="p">();</span>
        <span class="cp">#else
</span>          <span class="n">raise</span><span class="p">(</span><span class="n">SIGTRAP</span><span class="p">);</span>
        <span class="cp">#endif
</span>      <span class="cp">#endif
</span>    <span class="p">}</span>
  <span class="p">}</span>

  <span class="n">std</span><span class="o">::</span><span class="n">exit</span><span class="p">(</span><span class="n">EXIT_FAILURE</span><span class="p">);</span>
<span class="p">}</span>

<span class="n">application</span><span class="o">::</span><span class="n">application</span><span class="p">(</span><span class="kt">int</span> <span class="n">argc</span><span class="p">,</span> <span class="kt">char</span> <span class="o">**</span><span class="n">argv</span><span class="p">)</span> <span class="p">{</span>
  <span class="n">UNUSED</span><span class="p">(</span><span class="n">argc</span><span class="p">);</span>
  <span class="n">UNUSED</span><span class="p">(</span><span class="n">argv</span><span class="p">);</span>

  <span class="n">std</span><span class="o">::</span><span class="n">set_terminate</span><span class="p">(</span><span class="n">fail</span><span class="p">);</span>

  <span class="k">constexpr</span> <span class="k">const</span> <span class="k">auto</span> <span class="n">fn</span> <span class="o">=</span> <span class="p">[](</span><span class="kt">int</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">std</span><span class="o">::</span><span class="n">exit</span><span class="p">(</span><span class="n">EXIT_SUCCESS</span><span class="p">);</span>
  <span class="p">};</span>

  <span class="n">std</span><span class="o">::</span><span class="n">signal</span><span class="p">(</span><span class="n">SIGINT</span><span class="p">,</span> <span class="n">fn</span><span class="p">);</span>
  <span class="n">std</span><span class="o">::</span><span class="n">signal</span><span class="p">(</span><span class="n">SIGTERM</span><span class="p">,</span> <span class="n">fn</span><span class="p">);</span>

  <span class="cp">#ifdef HAVE_SENTRY
</span>    <span class="n">std</span><span class="o">::</span><span class="n">atexit</span><span class="p">([]</span> <span class="p">{</span> <span class="n">sentry_close</span><span class="p">();</span> <span class="p">});</span>
  <span class="cp">#endif
</span>
  <span class="n">std</span><span class="o">::</span><span class="n">atexit</span><span class="p">([]</span> <span class="p">{</span> <span class="n">PHYSFS_deinit</span><span class="p">();</span> <span class="p">});</span>
  <span class="n">std</span><span class="o">::</span><span class="n">atexit</span><span class="p">([]</span> <span class="p">{</span> <span class="n">SDL_Quit</span><span class="p">();</span> <span class="p">});</span>

  <span class="n">SDL_Init</span><span class="p">(</span><span class="n">SDL_INIT_GAMEPAD</span> <span class="o">|</span> <span class="n">SDL_INIT_VIDEO</span><span class="p">);</span>
  <span class="n">PHYSFS_init</span><span class="p">(</span><span class="n">argv</span><span class="p">[</span><span class="mi">0</span><span class="p">]);</span>
  <span class="cp">#ifdef HAVE_STEAM
</span>    <span class="n">SteamAPI_InitSafe</span><span class="p">();</span>
  <span class="cp">#endif
</span><span class="p">}</span>

<span class="kt">int32_t</span> <span class="n">application</span><span class="o">::</span><span class="n">run</span><span class="p">()</span> <span class="p">{</span>
  <span class="k">static_assert</span><span class="p">(</span><span class="n">std</span><span class="o">::</span><span class="n">endian</span><span class="o">::</span><span class="n">native</span> <span class="o">==</span> <span class="n">std</span><span class="o">::</span><span class="n">endian</span><span class="o">::</span><span class="n">little</span><span class="p">);</span>

  <span class="k">const</span> <span class="k">auto</span><span class="o">*</span> <span class="n">p</span> <span class="o">=</span> <span class="n">std</span><span class="o">::</span><span class="n">getenv</span><span class="p">(</span><span class="s">"CARTRIDGE"</span><span class="p">);</span>

  <span class="n">storage</span><span class="o">::</span><span class="n">filesystem</span><span class="o">::</span><span class="n">mount</span><span class="p">(</span><span class="n">p</span> <span class="o">?</span> <span class="n">p</span> <span class="o">:</span> <span class="s">"cartridge.zip"</span><span class="p">,</span> <span class="s">"/"</span><span class="p">);</span>

  <span class="k">auto</span> <span class="n">se</span> <span class="o">=</span> <span class="n">scriptengine</span><span class="p">();</span>
  <span class="n">se</span><span class="p">.</span><span class="n">run</span><span class="p">();</span>

  <span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="conclusion">Conclusion</h3>

<p>This way, I can provide Sentry support in a practically universal and abstract manner for the engine’s user.</p>

<p>You can find more details about the engine and its implementation in the official repository: <a href="https://github.com/willtobyte/carimbo">github.com/willtobyte/carimbo</a>.</p>]]></content><author><name>Rodrigo Delduca</name><email>rodrigo@delduca.org</email></author><summary type="html"><![CDATA[As I’ve already said countless times, I’m working on my first game for Steam, which you can check out online at reprobate.site.]]></summary></entry><entry><title type="html">Replacing Lua’s math.random module with the Xorshift algorithm</title><link href="https://nullonerror.org/2025/08/02/replacing-lua-s-math-random-module-with-the-xorshift-algorithm/" rel="alternate" type="text/html" title="Replacing Lua’s math.random module with the Xorshift algorithm" /><published>2025-08-02T00:00:00+00:00</published><updated>2025-08-02T00:00:00+00:00</updated><id>https://nullonerror.org/2025/08/02/replacing-lua-s-math-random-module-with-the-xorshift-algorithm</id><content type="html" xml:base="https://nullonerror.org/2025/08/02/replacing-lua-s-math-random-module-with-the-xorshift-algorithm/"><![CDATA[<p>I recently discovered that <em>V8</em>, the engine behind <em>Node.js</em> and <em>Chrome</em>, has been using <code class="language-plaintext highlighter-rouge">xorshift128+</code> since <strong><em>2014</em></strong>. Out of curiosity, I checked what the <em>Lua VM</em> uses, and to my surprise, it relies on the standard C library, which is extremely slow.</p>

<p>You can find more details here: <a href="https://github.com/v8/v8/blob/main/src/numbers/math-random.cc">v8/src/numbers/math-random.cc at main · v8/v8</a></p>

<p>I’m currently working on a <a href="https://reprobate.site/">game</a> that uses random in the main loop, which is quite slow. So to work around that, I implemented a pseudo-random function.</p>

<div class="language-lua highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">local</span> <span class="n">seed</span> <span class="o">=</span> <span class="nb">os.time</span><span class="p">()</span>
<span class="kd">local</span> <span class="k">function</span> <span class="nf">random</span><span class="p">()</span>
  <span class="n">seed</span> <span class="o">=</span> <span class="p">(</span><span class="mi">1103515245</span> <span class="o">*</span> <span class="n">seed</span> <span class="o">+</span> <span class="mi">12345</span><span class="p">)</span> <span class="o">%</span> <span class="mi">2147483648</span>
  <span class="k">return</span> <span class="n">seed</span>
<span class="k">end</span>
</code></pre></div></div>

<p>This is a <em>Linear Congruential Generator (LCG)</em> — one of the oldest and simplest methods for generating pseudorandom numbers.
For the noise effect, I end up calling random up to 6 times per loop, which is extremely costly, and not even the C++ implementation of <code class="language-plaintext highlighter-rouge">xorshift128+</code> below could save me.</p>

<p>Anyway, I ended up replacing Lua’s implementation with the one below, because optimizations are always welcome — especially when the gain in randomness is significantly higher.</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">static</span> <span class="n">std</span><span class="o">::</span><span class="n">array</span><span class="o">&lt;</span><span class="kt">uint64_t</span><span class="p">,</span> <span class="mi">2</span><span class="o">&gt;</span> <span class="n">prng_state</span><span class="p">;</span>

<span class="kt">void</span> <span class="nf">seed</span><span class="p">(</span><span class="kt">uint64_t</span> <span class="n">value</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">constexpr</span> <span class="kt">uint64_t</span> <span class="n">mix</span> <span class="o">=</span> <span class="mh">0xdeadbeefcafebabeULL</span><span class="p">;</span>
  <span class="k">if</span> <span class="p">(</span><span class="n">value</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span> <span class="n">value</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
  <span class="n">prng_state</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">=</span> <span class="n">value</span><span class="p">;</span>
  <span class="n">prng_state</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="o">=</span> <span class="n">value</span> <span class="o">^</span> <span class="n">mix</span><span class="p">;</span>
<span class="p">}</span>

<span class="kt">uint64_t</span> <span class="n">xorshift128plus</span><span class="p">()</span> <span class="p">{</span>
  <span class="k">const</span> <span class="k">auto</span> <span class="n">s1</span> <span class="o">=</span> <span class="n">prng_state</span><span class="p">[</span><span class="mi">0</span><span class="p">];</span>
  <span class="k">const</span> <span class="k">auto</span> <span class="n">s0</span> <span class="o">=</span> <span class="n">prng_state</span><span class="p">[</span><span class="mi">1</span><span class="p">];</span>
  <span class="k">const</span> <span class="k">auto</span> <span class="n">result</span> <span class="o">=</span> <span class="n">s0</span> <span class="o">+</span> <span class="n">s1</span><span class="p">;</span>

  <span class="n">prng_state</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">=</span> <span class="n">s0</span><span class="p">;</span>
  <span class="n">prng_state</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="o">=</span> <span class="p">(</span><span class="n">s1</span> <span class="o">^</span> <span class="p">(</span><span class="n">s1</span> <span class="o">&lt;&lt;</span> <span class="mi">23</span><span class="p">))</span> <span class="o">^</span> <span class="n">s0</span> <span class="o">^</span> <span class="p">((</span><span class="n">s1</span> <span class="o">^</span> <span class="p">(</span><span class="n">s1</span> <span class="o">&lt;&lt;</span> <span class="mi">23</span><span class="p">))</span> <span class="o">&gt;&gt;</span> <span class="mi">18</span><span class="p">)</span> <span class="o">^</span> <span class="p">(</span><span class="n">s0</span> <span class="o">&gt;&gt;</span> <span class="mi">5</span><span class="p">);</span>

  <span class="k">return</span> <span class="n">result</span><span class="p">;</span>
<span class="p">}</span>

<span class="kt">double</span> <span class="n">xorshift_random_double</span><span class="p">()</span> <span class="p">{</span>
  <span class="k">static</span> <span class="k">constexpr</span> <span class="k">const</span> <span class="k">auto</span> <span class="n">inv_max</span> <span class="o">=</span> <span class="mf">1.0</span> <span class="o">/</span> <span class="k">static_cast</span><span class="o">&lt;</span><span class="kt">double</span><span class="o">&gt;</span><span class="p">(</span><span class="n">std</span><span class="o">::</span><span class="n">numeric_limits</span><span class="o">&lt;</span><span class="kt">uint64_t</span><span class="o">&gt;::</span><span class="n">max</span><span class="p">());</span>

  <span class="k">return</span> <span class="k">static_cast</span><span class="o">&lt;</span><span class="kt">double</span><span class="o">&gt;</span><span class="p">(</span><span class="n">xorshift128plus</span><span class="p">())</span> <span class="o">*</span> <span class="n">inv_max</span><span class="p">;</span>
<span class="p">}</span>

<span class="n">lua_Integer</span> <span class="n">xorshift_random_int</span><span class="p">(</span><span class="k">const</span> <span class="n">lua_Integer</span> <span class="n">low</span><span class="p">,</span> <span class="k">const</span> <span class="n">lua_Integer</span> <span class="n">high</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">const</span> <span class="k">auto</span> <span class="n">ulow</span> <span class="o">=</span> <span class="k">static_cast</span><span class="o">&lt;</span><span class="kt">uint64_t</span><span class="o">&gt;</span><span class="p">(</span><span class="n">low</span><span class="p">);</span>
  <span class="k">const</span> <span class="k">auto</span> <span class="n">range</span> <span class="o">=</span> <span class="k">static_cast</span><span class="o">&lt;</span><span class="kt">uint64_t</span><span class="o">&gt;</span><span class="p">(</span><span class="n">high</span> <span class="o">-</span> <span class="n">low</span> <span class="o">+</span> <span class="mi">1</span><span class="p">);</span>
  <span class="k">return</span> <span class="k">static_cast</span><span class="o">&lt;</span><span class="n">lua_Integer</span><span class="o">&gt;</span><span class="p">(</span><span class="n">ulow</span> <span class="o">+</span> <span class="p">(</span><span class="n">xorshift128plus</span><span class="p">()</span> <span class="o">%</span> <span class="n">range</span><span class="p">));</span>
<span class="p">}</span>

<span class="k">const</span> <span class="k">auto</span> <span class="n">now</span> <span class="o">=</span> <span class="n">std</span><span class="o">::</span><span class="n">chrono</span><span class="o">::</span><span class="n">high_resolution_clock</span><span class="o">::</span><span class="n">now</span><span class="p">().</span><span class="n">time_since_epoch</span><span class="p">().</span><span class="n">count</span><span class="p">();</span>
<span class="n">seed</span><span class="p">(</span><span class="k">static_cast</span><span class="o">&lt;</span><span class="kt">uint64_t</span><span class="o">&gt;</span><span class="p">(</span><span class="n">now</span><span class="p">));</span>

<span class="n">lua</span><span class="p">[</span><span class="s">"math"</span><span class="p">][</span><span class="s">"random"</span><span class="p">]</span> <span class="o">=</span> <span class="n">sol</span><span class="o">::</span><span class="n">overload</span><span class="p">(</span>
  <span class="p">[]()</span> <span class="o">-&gt;</span> <span class="kt">double</span> <span class="p">{</span>
    <span class="k">return</span> <span class="n">xorshift_random_double</span><span class="p">();</span>
  <span class="p">},</span>
  <span class="p">[](</span><span class="n">lua_Integer</span> <span class="n">upper</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">lua_Integer</span> <span class="p">{</span>
    <span class="k">return</span> <span class="n">xorshift_random_int</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="n">upper</span><span class="p">);</span>
  <span class="p">},</span>
  <span class="p">[](</span><span class="n">lua_Integer</span> <span class="n">lower</span><span class="p">,</span> <span class="n">lua_Integer</span> <span class="n">upper</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">lua_Integer</span> <span class="p">{</span>
    <span class="k">return</span> <span class="n">xorshift_random_int</span><span class="p">(</span><span class="n">lower</span><span class="p">,</span> <span class="n">upper</span><span class="p">);</span>
  <span class="p">}</span>
<span class="p">);</span>

<span class="n">lua</span><span class="p">[</span><span class="s">"math"</span><span class="p">][</span><span class="s">"randomseed"</span><span class="p">]</span> <span class="o">=</span> <span class="p">[](</span><span class="n">lua_Integer</span> <span class="n">seed_value</span><span class="p">)</span> <span class="p">{</span>
  <span class="n">seed</span><span class="p">(</span><span class="k">static_cast</span><span class="o">&lt;</span><span class="kt">uint64_t</span><span class="o">&gt;</span><span class="p">(</span><span class="n">seed_value</span><span class="p">));</span>
<span class="p">};</span>
</code></pre></div></div>]]></content><author><name>Rodrigo Delduca</name><email>rodrigo@delduca.org</email></author><summary type="html"><![CDATA[I recently discovered that V8, the engine behind Node.js and Chrome, has been using xorshift128+ since 2014. Out of curiosity, I checked what the Lua VM uses, and to my surprise, it relies on the standard C library, which is extremely slow.]]></summary></entry><entry><title type="html">Poor’s Man Shaders</title><link href="https://nullonerror.org/2025/07/29/poor-s-man-shaders/" rel="alternate" type="text/html" title="Poor’s Man Shaders" /><published>2025-07-29T00:00:00+00:00</published><updated>2025-07-29T00:00:00+00:00</updated><id>https://nullonerror.org/2025/07/29/poor-s-man-shaders</id><content type="html" xml:base="https://nullonerror.org/2025/07/29/poor-s-man-shaders/"><![CDATA[<blockquote>
  <p>Spoiler: it’s not shaders</p>
</blockquote>

<p>I’m waiting for a universal solution that the SDL developers are working on — cross-platform, multi-API shaders, the <a href="https://github.com/libsdl-org/SDL_shadercross">SDL_shadercross</a>. The idea is that you write shaders in a single language, and at runtime, they get compiled for the target GPU. Unfortunately, it’s a large and complex project, and it will take time before it becomes stable.</p>

<p>In the meantime, in my <a href="https://github.com/willtobyte/carimbo">Carimbo</a>  engine, I was wondering if I could implement something similar to shaders — something that would allow Lua code to write arbitrary pixels into a buffer and stream that buffer into a texture.</p>

<p>So I created what I call a canvas, which is basically a texture the same size as the screen, rendered after certain elements.</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">canvas</span><span class="o">::</span><span class="n">canvas</span><span class="p">(</span><span class="n">std</span><span class="o">::</span><span class="n">shared_ptr</span><span class="o">&lt;</span><span class="n">renderer</span><span class="o">&gt;</span> <span class="n">renderer</span><span class="p">)</span>
    <span class="o">:</span> <span class="n">_renderer</span><span class="p">(</span><span class="n">std</span><span class="o">::</span><span class="n">move</span><span class="p">(</span><span class="n">renderer</span><span class="p">))</span> <span class="p">{</span>
  <span class="kt">int32_t</span> <span class="n">lw</span><span class="p">,</span> <span class="n">lh</span><span class="p">;</span>
  <span class="n">SDL_RendererLogicalPresentation</span> <span class="n">mode</span><span class="p">;</span>
  <span class="n">SDL_GetRenderLogicalPresentation</span><span class="p">(</span><span class="o">*</span><span class="n">_renderer</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">lw</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">lh</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">mode</span><span class="p">);</span>

  <span class="n">float_t</span> <span class="n">sx</span><span class="p">,</span> <span class="n">sy</span><span class="p">;</span>
  <span class="n">SDL_GetRenderScale</span><span class="p">(</span><span class="o">*</span><span class="n">_renderer</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">sx</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">sy</span><span class="p">);</span>

  <span class="k">const</span> <span class="k">auto</span> <span class="n">width</span> <span class="o">=</span> <span class="k">static_cast</span><span class="o">&lt;</span><span class="kt">int32_t</span><span class="o">&gt;</span><span class="p">(</span><span class="n">std</span><span class="o">::</span><span class="n">lround</span><span class="p">(</span><span class="k">static_cast</span><span class="o">&lt;</span><span class="kt">float</span><span class="o">&gt;</span><span class="p">(</span><span class="n">lw</span><span class="p">)</span> <span class="o">/</span> <span class="n">sx</span><span class="p">));</span>
  <span class="k">const</span> <span class="k">auto</span> <span class="n">height</span> <span class="o">=</span> <span class="k">static_cast</span><span class="o">&lt;</span><span class="kt">int32_t</span><span class="o">&gt;</span><span class="p">(</span><span class="n">std</span><span class="o">::</span><span class="n">lround</span><span class="p">(</span><span class="k">static_cast</span><span class="o">&lt;</span><span class="kt">float</span><span class="o">&gt;</span><span class="p">(</span><span class="n">lh</span><span class="p">)</span> <span class="o">/</span> <span class="n">sy</span><span class="p">));</span>

  <span class="n">SDL_Texture</span> <span class="o">*</span><span class="n">texture</span> <span class="o">=</span> <span class="n">SDL_CreateTexture</span><span class="p">(</span><span class="o">*</span><span class="n">_renderer</span><span class="p">,</span> <span class="n">SDL_PIXELFORMAT_ARGB8888</span><span class="p">,</span> <span class="n">SDL_TEXTUREACCESS_STREAMING</span><span class="p">,</span> <span class="n">width</span><span class="p">,</span> <span class="n">height</span><span class="p">);</span>
  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">texture</span><span class="p">)</span> <span class="p">[[</span><span class="n">unlikely</span><span class="p">]]</span> <span class="p">{</span>
    <span class="k">throw</span> <span class="n">std</span><span class="o">::</span><span class="n">runtime_error</span><span class="p">(</span><span class="n">std</span><span class="o">::</span><span class="n">format</span><span class="p">(</span><span class="s">"[SDL_CreateTexture] {}"</span><span class="p">,</span> <span class="n">SDL_GetError</span><span class="p">()));</span>
  <span class="p">}</span>

  <span class="n">SDL_SetTextureBlendMode</span><span class="p">(</span><span class="n">texture</span><span class="p">,</span> <span class="n">SDL_BLENDMODE_BLEND</span><span class="p">);</span>

  <span class="n">_framebuffer</span><span class="p">.</span><span class="n">reset</span><span class="p">(</span><span class="n">texture</span><span class="p">);</span>
<span class="p">}</span>

<span class="kt">void</span> <span class="n">canvas</span><span class="o">::</span><span class="n">set_pixels</span><span class="p">(</span><span class="k">const</span> <span class="kt">uint32_t</span><span class="o">*</span> <span class="n">pixels</span><span class="p">)</span> <span class="k">noexcept</span> <span class="p">{</span>
  <span class="k">const</span> <span class="k">auto</span> <span class="n">ptr</span> <span class="o">=</span> <span class="n">_framebuffer</span><span class="p">.</span><span class="n">get</span><span class="p">();</span>
  <span class="k">const</span> <span class="k">auto</span> <span class="n">pitch</span> <span class="o">=</span> <span class="k">static_cast</span><span class="o">&lt;</span><span class="kt">int</span><span class="o">&gt;</span><span class="p">(</span><span class="k">static_cast</span><span class="o">&lt;</span><span class="kt">size_t</span><span class="o">&gt;</span><span class="p">(</span><span class="n">ptr</span><span class="o">-&gt;</span><span class="n">w</span><span class="p">)</span> <span class="o">*</span> <span class="k">sizeof</span><span class="p">(</span><span class="kt">uint32_t</span><span class="p">));</span>

  <span class="n">SDL_UpdateTexture</span><span class="p">(</span><span class="n">ptr</span><span class="p">,</span> <span class="nb">nullptr</span><span class="p">,</span> <span class="n">pixels</span><span class="p">,</span> <span class="n">pitch</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The set_pixel function receives a pointer to a uint32_t buffer that exactly matches the texture size. This pointer is actually a Lua string, which I found to be the most performant way to transfer data between Lua and C++ without relying on preallocated buffers.</p>

<p>This way:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">lua</span><span class="p">.</span><span class="n">new_usertype</span><span class="o">&lt;</span><span class="n">graphics</span><span class="o">::</span><span class="n">canvas</span><span class="o">&gt;</span><span class="p">(</span>
  <span class="s">"Canvas"</span><span class="p">,</span>
  <span class="n">sol</span><span class="o">::</span><span class="n">no_constructor</span><span class="p">,</span>
  <span class="s">"pixels"</span><span class="p">,</span> <span class="n">sol</span><span class="o">::</span><span class="n">property</span><span class="p">(</span>
    <span class="p">[](</span><span class="k">const</span> <span class="n">graphics</span><span class="o">::</span><span class="n">canvas</span><span class="o">&amp;</span><span class="p">)</span> <span class="p">{</span>
      <span class="k">return</span> <span class="nb">nullptr</span><span class="p">;</span>
    <span class="p">},</span>
    <span class="p">[](</span><span class="n">graphics</span><span class="o">::</span><span class="n">canvas</span><span class="o">&amp;</span> <span class="n">canvas</span><span class="p">,</span> <span class="k">const</span> <span class="kt">char</span><span class="o">*</span> <span class="n">data</span><span class="p">)</span> <span class="p">{</span>
      <span class="n">canvas</span><span class="p">.</span><span class="n">set_pixels</span><span class="p">(</span><span class="k">reinterpret_cast</span><span class="o">&lt;</span><span class="k">const</span> <span class="kt">uint32_t</span><span class="o">*&gt;</span><span class="p">(</span><span class="n">data</span><span class="p">));</span>
    <span class="p">}</span>
  <span class="p">)</span>
<span class="p">);</span>
</code></pre></div></div>

<p>On Lua side:</p>

<div class="language-lua highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">function</span> <span class="nf">Effect</span><span class="p">:</span><span class="n">new</span><span class="p">(</span><span class="n">width</span><span class="p">,</span> <span class="n">height</span><span class="p">)</span>
  <span class="kd">local</span> <span class="n">w</span><span class="p">,</span> <span class="n">h</span> <span class="o">=</span> <span class="n">width</span> <span class="ow">or</span> <span class="mi">480</span><span class="p">,</span> <span class="n">height</span> <span class="ow">or</span> <span class="mi">270</span>
  <span class="kd">local</span> <span class="n">canvas</span> <span class="o">=</span> <span class="n">engine</span><span class="p">:</span><span class="n">canvas</span><span class="p">()</span>
  <span class="kd">local</span> <span class="n">self</span> <span class="o">=</span> <span class="nb">setmetatable</span><span class="p">({</span>
    <span class="n">canvas</span> <span class="o">=</span> <span class="n">canvas</span><span class="p">,</span>
    <span class="n">w</span> <span class="o">=</span> <span class="n">w</span><span class="p">,</span>
    <span class="n">h</span> <span class="o">=</span> <span class="n">h</span><span class="p">,</span>
  <span class="p">},</span> <span class="n">Effect</span><span class="p">)</span>

  <span class="k">return</span> <span class="n">self</span>
<span class="k">end</span>

<span class="k">function</span> <span class="nf">Effect</span><span class="p">:</span><span class="n">loop</span><span class="p">()</span>
  <span class="c1">-- Some cool effect...</span>
  <span class="n">self</span><span class="p">.</span><span class="n">canvas</span><span class="p">.</span><span class="n">pixels</span> <span class="o">=</span> <span class="n">rep</span><span class="p">(</span><span class="n">char</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">220</span><span class="p">),</span> <span class="n">self</span><span class="p">.</span><span class="n">w</span> <span class="o">*</span> <span class="n">self</span><span class="p">.</span><span class="n">h</span><span class="p">)</span><span class="err">`</span>
<span class="k">end</span>

</code></pre></div></div>

<p>Some effects I’ve created so far:</p>

<p><a href="https://youtu.be/GUWTWRQuzxw">https://youtu.be/GUWTWRQuzxw</a></p>

<p><a href="https://youtu.be/usJ9QM7V8BI">https://youtu.be/usJ9QM7V8BI</a></p>

<p><a href="https://youtu.be/DUhQmL91cNA">https://youtu.be/DUhQmL91cNA</a></p>]]></content><author><name>Rodrigo Delduca</name><email>rodrigo@delduca.org</email></author><summary type="html"><![CDATA[Spoiler: it’s not shaders]]></summary></entry><entry><title type="html">AI will replace programmers—just not yet, because it still generates very extremely inefficient code.</title><link href="https://nullonerror.org/2025/07/12/ai-will-replace-programmers-just-not-yet-because-it-currently-generates-extremely-inefficient-code/" rel="alternate" type="text/html" title="AI will replace programmers—just not yet, because it still generates very extremely inefficient code." /><published>2025-07-12T00:00:00+00:00</published><updated>2025-07-12T00:00:00+00:00</updated><id>https://nullonerror.org/2025/07/12/ai-will-replace-programmers-just-not-yet-because-it-currently-generates-extremely-inefficient-code</id><content type="html" xml:base="https://nullonerror.org/2025/07/12/ai-will-replace-programmers-just-not-yet-because-it-currently-generates-extremely-inefficient-code/"><![CDATA[<p>I was working on my engine, which includes a sort of canvas where Lua code can generate chunks of pixels and send them in batches for the C++ engine to render.
This worked very well and smoothly at 60 frames per second with no frame drops at low resolutions (240p, which is the screen size of my games).
However, when I happened to try 1080p, the frame rate dropped.</p>

<p>Since I was in a rush and a bit lazy—because I can’t afford to spend too much time on personal projects—I decided to use AI to optimize it, and this was the best solution I could squeeze out.
It went from 40 FPS down to 17, much worse than the initial implementation!</p>

<p>AI Code:</p>

<div class="language-c++ highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[](</span><span class="n">graphics</span><span class="o">::</span><span class="n">canvas</span><span class="o">&amp;</span> <span class="n">canvas</span><span class="p">,</span> <span class="n">sol</span><span class="o">::</span><span class="n">table</span> <span class="n">table</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">const</span> <span class="k">auto</span> <span class="n">n</span> <span class="o">=</span> <span class="n">table</span><span class="p">.</span><span class="n">size</span><span class="p">();</span>
  <span class="k">static</span> <span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="kt">uint32_t</span><span class="o">&gt;</span> <span class="n">buffer</span><span class="p">;</span>

  <span class="k">if</span> <span class="p">(</span><span class="n">buffer</span><span class="p">.</span><span class="n">size</span><span class="p">()</span> <span class="o">!=</span> <span class="n">n</span><span class="p">)</span> <span class="p">[[</span><span class="n">unlikely</span><span class="p">]]</span> <span class="p">{</span>
    <span class="n">buffer</span><span class="p">.</span><span class="n">resize</span><span class="p">(</span><span class="n">n</span><span class="p">);</span>
  <span class="p">}</span>

  <span class="k">const</span> <span class="k">auto</span> <span class="n">L</span> <span class="o">=</span> <span class="n">table</span><span class="p">.</span><span class="n">lua_state</span><span class="p">();</span>

  <span class="n">table</span><span class="p">.</span><span class="n">push</span><span class="p">();</span>
  <span class="k">const</span> <span class="kt">int</span> <span class="n">table_idx</span> <span class="o">=</span> <span class="n">lua_gettop</span><span class="p">(</span><span class="n">L</span><span class="p">);</span>

  <span class="kt">uint32_t</span><span class="o">*</span> <span class="k">const</span> <span class="n">data</span> <span class="o">=</span> <span class="n">buffer</span><span class="p">.</span><span class="n">data</span><span class="p">();</span>

  <span class="k">constexpr</span> <span class="n">std</span><span class="o">::</span><span class="kt">size_t</span> <span class="n">batch_size</span> <span class="o">=</span> <span class="mi">8</span><span class="p">;</span>
  <span class="k">const</span> <span class="n">std</span><span class="o">::</span><span class="kt">size_t</span> <span class="n">full_batches</span> <span class="o">=</span> <span class="n">n</span> <span class="o">/</span> <span class="n">batch_size</span><span class="p">;</span>

  <span class="k">for</span> <span class="p">(</span><span class="n">std</span><span class="o">::</span><span class="kt">size_t</span> <span class="n">batch</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">batch</span> <span class="o">&lt;</span> <span class="n">full_batches</span><span class="p">;</span> <span class="o">++</span><span class="n">batch</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">const</span> <span class="n">std</span><span class="o">::</span><span class="kt">size_t</span> <span class="n">start_idx</span> <span class="o">=</span> <span class="n">batch</span> <span class="o">*</span> <span class="n">batch_size</span><span class="p">;</span>

    <span class="k">for</span> <span class="p">(</span><span class="n">std</span><span class="o">::</span><span class="kt">size_t</span> <span class="n">j</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">j</span> <span class="o">&lt;</span> <span class="n">batch_size</span><span class="p">;</span> <span class="o">++</span><span class="n">j</span><span class="p">)</span> <span class="p">{</span>
      <span class="n">lua_rawgeti</span><span class="p">(</span><span class="n">L</span><span class="p">,</span> <span class="n">table_idx</span><span class="p">,</span> <span class="k">static_cast</span><span class="o">&lt;</span><span class="kt">int</span><span class="o">&gt;</span><span class="p">(</span><span class="n">start_idx</span> <span class="o">+</span> <span class="n">j</span> <span class="o">+</span> <span class="mi">1</span><span class="p">));</span>
    <span class="p">}</span>

    <span class="k">for</span> <span class="p">(</span><span class="n">std</span><span class="o">::</span><span class="kt">size_t</span> <span class="n">j</span> <span class="o">=</span> <span class="n">batch_size</span><span class="p">;</span> <span class="n">j</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">;</span> <span class="o">--</span><span class="n">j</span><span class="p">)</span> <span class="p">{</span>
      <span class="n">data</span><span class="p">[</span><span class="n">start_idx</span> <span class="o">+</span> <span class="n">j</span> <span class="o">-</span> <span class="mi">1</span><span class="p">]</span> <span class="o">=</span> <span class="k">static_cast</span><span class="o">&lt;</span><span class="kt">uint32_t</span><span class="o">&gt;</span><span class="p">(</span><span class="n">lua_tointeger</span><span class="p">(</span><span class="n">L</span><span class="p">,</span> <span class="o">-</span><span class="mi">1</span><span class="p">));</span>
      <span class="n">lua_pop</span><span class="p">(</span><span class="n">L</span><span class="p">,</span> <span class="mi">1</span><span class="p">);</span>
    <span class="p">}</span>
  <span class="p">}</span>

  <span class="k">const</span> <span class="n">std</span><span class="o">::</span><span class="kt">size_t</span> <span class="n">remaining_start</span> <span class="o">=</span> <span class="n">full_batches</span> <span class="o">*</span> <span class="n">batch_size</span><span class="p">;</span>
  <span class="k">for</span> <span class="p">(</span><span class="n">std</span><span class="o">::</span><span class="kt">size_t</span> <span class="n">i</span> <span class="o">=</span> <span class="n">remaining_start</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">n</span><span class="p">;</span> <span class="o">++</span><span class="n">i</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">lua_rawgeti</span><span class="p">(</span><span class="n">L</span><span class="p">,</span> <span class="n">table_idx</span><span class="p">,</span> <span class="k">static_cast</span><span class="o">&lt;</span><span class="kt">int</span><span class="o">&gt;</span><span class="p">(</span><span class="n">i</span> <span class="o">+</span> <span class="mi">1</span><span class="p">));</span>
    <span class="n">data</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">=</span> <span class="k">static_cast</span><span class="o">&lt;</span><span class="kt">uint32_t</span><span class="o">&gt;</span><span class="p">(</span><span class="n">lua_tointeger</span><span class="p">(</span><span class="n">L</span><span class="p">,</span> <span class="o">-</span><span class="mi">1</span><span class="p">));</span>
    <span class="n">lua_pop</span><span class="p">(</span><span class="n">L</span><span class="p">,</span> <span class="mi">1</span><span class="p">);</span>
  <span class="p">}</span>

  <span class="n">lua_pop</span><span class="p">(</span><span class="n">L</span><span class="p">,</span> <span class="mi">1</span><span class="p">);</span>
  <span class="n">canvas</span><span class="p">.</span><span class="n">set_pixels</span><span class="p">(</span><span class="n">buffer</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Naturally, the code was not just complex, but also way slower.</p>

<p>That’s when I decided to take my brain off the shelf and came up with this solution:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[](</span><span class="n">graphics</span><span class="o">::</span><span class="n">canvas</span><span class="o">&amp;</span> <span class="n">canvas</span><span class="p">,</span> <span class="k">const</span> <span class="kt">char</span><span class="o">*</span> <span class="n">data</span><span class="p">)</span> <span class="p">{</span>
  <span class="n">canvas</span><span class="p">.</span><span class="n">set_pixels</span><span class="p">(</span><span class="k">reinterpret_cast</span><span class="o">&lt;</span><span class="k">const</span> <span class="kt">uint32_t</span><span class="o">*&gt;</span><span class="p">(</span><span class="n">data</span><span class="p">));</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Kabum! Smooth 60 frames per second, even at 8K resolution or higher.</p>]]></content><author><name>Rodrigo Delduca</name><email>rodrigo@delduca.org</email></author><summary type="html"><![CDATA[I was working on my engine, which includes a sort of canvas where Lua code can generate chunks of pixels and send them in batches for the C++ engine to render. This worked very well and smoothly at 60 frames per second with no frame drops at low resolutions (240p, which is the screen size of my games). However, when I happened to try 1080p, the frame rate dropped.]]></summary></entry><entry><title type="html">How to avoid dynamic linking of Steam’s client library using a very old trick</title><link href="https://nullonerror.org/2025/06/03/how-to-avoid-dynamic-linking-of-steam-s-client-library-using-a-very-old-trick/" rel="alternate" type="text/html" title="How to avoid dynamic linking of Steam’s client library using a very old trick" /><published>2025-06-03T00:00:00+00:00</published><updated>2025-06-03T00:00:00+00:00</updated><id>https://nullonerror.org/2025/06/03/how-to-avoid-dynamic-linking-of-steam-s-client-library-using-a-very-old-trick</id><content type="html" xml:base="https://nullonerror.org/2025/06/03/how-to-avoid-dynamic-linking-of-steam-s-client-library-using-a-very-old-trick/"><![CDATA[<p>As you know, this blog is more focused on sharing code snippets than on teaching, so today I’m going to show you something I recently discovered.</p>

<p>If you’ve been following me, you know I’ve been working in my free time on a 2D game engine where creators can build games using only Lua — and I’d say, even fairly complex ones.</p>

<p>Right now, I’m working on a point-and-click game that you can play here: https://bereprobate.com/. It’s built using this same engine, and I’m publishing builds in parallel to Steam and the Web using GitHub Actions.</p>

<p>The thing is, Steam — which is the main target platform for this game — supports achievements, and I want to include them. But to use achievements, you have to link the Steam library to your engine. The problem is, doing that creates a dependency on that library in the binaries, which I don’t want. I also don’t want to maintain a separate build just for that.</p>

<p>Then I thought: “Why not load the Steam library dynamically? Use LoadLibraryA on Windows and dlopen on macOS. (Sorry Linux — it’s Proton-only for now.)”</p>

<p>I tried the experiment below, and it worked. If the DLL/dylib is present, the Steam features work just fine. If not, everything runs normally.</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#include</span> <span class="cpf">"steam.hpp"</span><span class="cp">
</span>
<span class="cp">#if defined(_WIN32)
</span>  <span class="cp">#include</span> <span class="cpf">&lt;windows.h&gt;</span><span class="cp">
</span>  <span class="cp">#define DYNLIB_HANDLE HMODULE
</span>  <span class="cp">#define DYNLIB_LOAD(name) LoadLibraryA(name)
</span>  <span class="cp">#define DYNLIB_SYM(lib, name) GetProcAddress(lib, name)
</span>  <span class="cp">#define STEAM_LIB_NAME "steam_api64.dll"
#elif defined(__APPLE__)
</span>  <span class="cp">#include</span> <span class="cpf">&lt;dlfcn.h&gt;</span><span class="cp">
</span>  <span class="cp">#define DYNLIB_HANDLE void*
</span>  <span class="cp">#define DYNLIB_LOAD(name) dlopen(name, RTLD_LAZY)
</span>  <span class="cp">#define DYNLIB_SYM(lib, name) dlsym(lib, name)
</span>  <span class="cp">#define STEAM_LIB_NAME "libsteam_api.dylib"
#endif
</span>
<span class="cp">#ifndef S_CALLTYPE
</span>  <span class="cp">#define S_CALLTYPE __cdecl
#endif
</span>
<span class="cp">#if defined(DYNLIB_LOAD)
</span>
<span class="k">using</span> <span class="n">SteamAPI_InitSafe_t</span>     <span class="o">=</span> <span class="kt">bool</span><span class="p">(</span><span class="n">S_CALLTYPE</span> <span class="o">*</span><span class="p">)();</span>
<span class="k">using</span> <span class="n">SteamAPI_Shutdown_t</span>     <span class="o">=</span> <span class="kt">void</span><span class="p">(</span><span class="n">S_CALLTYPE</span> <span class="o">*</span><span class="p">)();</span>
<span class="k">using</span> <span class="n">SteamAPI_RunCallbacks_t</span> <span class="o">=</span> <span class="kt">void</span><span class="p">(</span><span class="n">S_CALLTYPE</span> <span class="o">*</span><span class="p">)();</span>
<span class="k">using</span> <span class="n">SteamUserStats_t</span>        <span class="o">=</span> <span class="kt">void</span><span class="o">*</span><span class="p">(</span><span class="n">S_CALLTYPE</span> <span class="o">*</span><span class="p">)();</span>
<span class="k">using</span> <span class="n">GetAchievement_t</span>        <span class="o">=</span> <span class="kt">bool</span><span class="p">(</span><span class="n">S_CALLTYPE</span> <span class="o">*</span><span class="p">)(</span><span class="kt">void</span><span class="o">*</span><span class="p">,</span> <span class="k">const</span> <span class="kt">char</span><span class="o">*</span><span class="p">,</span> <span class="kt">bool</span><span class="o">*</span><span class="p">);</span>
<span class="k">using</span> <span class="n">SetAchievement_t</span>        <span class="o">=</span> <span class="kt">bool</span><span class="p">(</span><span class="n">S_CALLTYPE</span> <span class="o">*</span><span class="p">)(</span><span class="kt">void</span><span class="o">*</span><span class="p">,</span> <span class="k">const</span> <span class="kt">char</span><span class="o">*</span><span class="p">);</span>
<span class="k">using</span> <span class="n">StoreStats_t</span>            <span class="o">=</span> <span class="kt">bool</span><span class="p">(</span><span class="n">S_CALLTYPE</span> <span class="o">*</span><span class="p">)(</span><span class="kt">void</span><span class="o">*</span><span class="p">);</span>

<span class="k">static</span> <span class="n">DYNLIB_HANDLE</span> <span class="n">hSteamApi</span> <span class="o">=</span> <span class="n">DYNLIB_LOAD</span><span class="p">(</span><span class="n">STEAM_LIB_NAME</span><span class="p">);</span>

<span class="cp">#define LOAD_SYMBOL(name, sym) reinterpret_cast&lt;name&gt;(reinterpret_cast&lt;void*&gt;(DYNLIB_SYM(hSteamApi, sym)))
</span>
<span class="k">static</span> <span class="k">const</span> <span class="k">auto</span> <span class="n">pSteamAPI_InitSafe</span>     <span class="o">=</span> <span class="n">LOAD_SYMBOL</span><span class="p">(</span><span class="n">SteamAPI_InitSafe_t</span><span class="p">,</span> <span class="s">"SteamAPI_InitSafe"</span><span class="p">);</span>
<span class="k">static</span> <span class="k">const</span> <span class="k">auto</span> <span class="n">pSteamAPI_Shutdown</span>     <span class="o">=</span> <span class="n">LOAD_SYMBOL</span><span class="p">(</span><span class="n">SteamAPI_Shutdown_t</span><span class="p">,</span> <span class="s">"SteamAPI_Shutdown"</span><span class="p">);</span>
<span class="k">static</span> <span class="k">const</span> <span class="k">auto</span> <span class="n">pSteamAPI_RunCallbacks</span> <span class="o">=</span> <span class="n">LOAD_SYMBOL</span><span class="p">(</span><span class="n">SteamAPI_RunCallbacks_t</span><span class="p">,</span> <span class="s">"SteamAPI_RunCallbacks"</span><span class="p">);</span>
<span class="k">static</span> <span class="k">const</span> <span class="k">auto</span> <span class="n">pSteamUserStats</span>        <span class="o">=</span> <span class="n">LOAD_SYMBOL</span><span class="p">(</span><span class="n">SteamUserStats_t</span><span class="p">,</span> <span class="s">"SteamAPI_SteamUserStats_v013"</span><span class="p">);</span>
<span class="k">static</span> <span class="k">const</span> <span class="k">auto</span> <span class="n">pGetAchievement</span>        <span class="o">=</span> <span class="n">LOAD_SYMBOL</span><span class="p">(</span><span class="n">GetAchievement_t</span><span class="p">,</span> <span class="s">"SteamAPI_ISteamUserStats_GetAchievement"</span><span class="p">);</span>
<span class="k">static</span> <span class="k">const</span> <span class="k">auto</span> <span class="n">pSetAchievement</span>        <span class="o">=</span> <span class="n">LOAD_SYMBOL</span><span class="p">(</span><span class="n">SetAchievement_t</span><span class="p">,</span> <span class="s">"SteamAPI_ISteamUserStats_SetAchievement"</span><span class="p">);</span>
<span class="k">static</span> <span class="k">const</span> <span class="k">auto</span> <span class="n">pStoreStats</span>            <span class="o">=</span> <span class="n">LOAD_SYMBOL</span><span class="p">(</span><span class="n">StoreStats_t</span><span class="p">,</span> <span class="s">"SteamAPI_ISteamUserStats_StoreStats"</span><span class="p">);</span>

<span class="kt">bool</span> <span class="nf">SteamAPI_InitSafe</span><span class="p">()</span> <span class="p">{</span>
  <span class="k">return</span> <span class="n">pSteamAPI_InitSafe</span> <span class="o">?</span> <span class="n">pSteamAPI_InitSafe</span><span class="p">()</span> <span class="o">:</span> <span class="nb">false</span><span class="p">;</span>
<span class="p">}</span>

<span class="kt">void</span> <span class="n">SteamAPI_Shutdown</span><span class="p">()</span> <span class="p">{</span>
  <span class="n">pSteamAPI_Shutdown</span> <span class="o">?</span> <span class="n">pSteamAPI_Shutdown</span><span class="p">()</span> <span class="o">:</span> <span class="kt">void</span><span class="p">();</span>
<span class="p">}</span>

<span class="kt">void</span> <span class="n">SteamAPI_RunCallbacks</span><span class="p">()</span> <span class="p">{</span>
  <span class="n">pSteamAPI_RunCallbacks</span> <span class="o">?</span> <span class="n">pSteamAPI_RunCallbacks</span><span class="p">()</span> <span class="o">:</span> <span class="kt">void</span><span class="p">();</span>
<span class="p">}</span>

<span class="kt">void</span><span class="o">*</span> <span class="n">SteamUserStats</span><span class="p">()</span> <span class="p">{</span>
  <span class="k">return</span> <span class="n">pSteamUserStats</span> <span class="o">?</span> <span class="n">pSteamUserStats</span><span class="p">()</span> <span class="o">:</span> <span class="nb">nullptr</span><span class="p">;</span>
<span class="p">}</span>

<span class="kt">bool</span> <span class="n">GetAchievement</span><span class="p">(</span><span class="k">const</span> <span class="kt">char</span><span class="o">*</span> <span class="n">name</span><span class="p">)</span> <span class="p">{</span>
  <span class="kt">bool</span> <span class="n">achieved</span> <span class="o">=</span> <span class="nb">false</span><span class="p">;</span>
  <span class="k">return</span> <span class="n">pGetAchievement</span> <span class="o">?</span> <span class="p">(</span><span class="n">pGetAchievement</span><span class="p">(</span><span class="n">SteamUserStats</span><span class="p">(),</span> <span class="n">name</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">achieved</span><span class="p">)</span> <span class="o">&amp;&amp;</span> <span class="n">achieved</span><span class="p">)</span> <span class="o">:</span> <span class="nb">false</span><span class="p">;</span>
<span class="p">}</span>

<span class="kt">bool</span> <span class="n">SetAchievement</span><span class="p">(</span><span class="k">const</span> <span class="kt">char</span><span class="o">*</span> <span class="n">name</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">return</span> <span class="n">pSetAchievement</span> <span class="o">?</span> <span class="n">pSetAchievement</span><span class="p">(</span><span class="n">SteamUserStats</span><span class="p">(),</span> <span class="n">name</span><span class="p">)</span> <span class="o">:</span> <span class="nb">false</span><span class="p">;</span>
<span class="p">}</span>

<span class="kt">bool</span> <span class="n">StoreStats</span><span class="p">()</span> <span class="p">{</span>
  <span class="k">return</span> <span class="n">pStoreStats</span> <span class="o">?</span> <span class="n">pStoreStats</span><span class="p">(</span><span class="n">SteamUserStats</span><span class="p">())</span> <span class="o">:</span> <span class="nb">false</span><span class="p">;</span>
<span class="p">}</span>

<span class="cp">#else
</span>
<span class="kt">bool</span> <span class="n">SteamAPI_InitSafe</span><span class="p">()</span>            <span class="p">{</span> <span class="k">return</span> <span class="nb">false</span><span class="p">;</span> <span class="p">}</span>
<span class="kt">void</span> <span class="n">SteamAPI_Shutdown</span><span class="p">()</span>           <span class="p">{}</span>
<span class="kt">void</span> <span class="n">SteamAPI_RunCallbacks</span><span class="p">()</span>       <span class="p">{}</span>
<span class="kt">void</span><span class="o">*</span> <span class="n">SteamUserStats</span><span class="p">()</span>             <span class="p">{</span> <span class="k">return</span> <span class="nb">nullptr</span><span class="p">;</span> <span class="p">}</span>
<span class="kt">bool</span> <span class="n">GetAchievement</span><span class="p">(</span><span class="k">const</span> <span class="kt">char</span><span class="o">*</span><span class="p">)</span>   <span class="p">{</span> <span class="k">return</span> <span class="nb">false</span><span class="p">;</span> <span class="p">}</span>
<span class="kt">bool</span> <span class="n">SetAchievement</span><span class="p">(</span><span class="k">const</span> <span class="kt">char</span><span class="o">*</span><span class="p">)</span>   <span class="p">{</span> <span class="k">return</span> <span class="nb">false</span><span class="p">;</span> <span class="p">}</span>
<span class="kt">bool</span> <span class="n">StoreStats</span><span class="p">()</span>                  <span class="p">{</span> <span class="k">return</span> <span class="nb">false</span><span class="p">;</span> <span class="p">}</span>

<span class="cp">#endif
</span></code></pre></div></div>

<p>Achivement class</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">void</span> <span class="n">achievement</span><span class="o">::</span><span class="n">unlock</span><span class="p">(</span><span class="n">std</span><span class="o">::</span><span class="n">string</span> <span class="n">id</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">SteamUserStats</span><span class="p">())</span> <span class="p">{</span>
    <span class="k">return</span><span class="p">;</span>
  <span class="p">}</span>

  <span class="k">const</span> <span class="k">auto</span><span class="o">*</span> <span class="n">ptr</span> <span class="o">=</span> <span class="n">id</span><span class="p">.</span><span class="n">c_str</span><span class="p">();</span>

  <span class="k">if</span> <span class="p">(</span><span class="n">GetAchievement</span><span class="p">(</span><span class="n">ptr</span><span class="p">))</span> <span class="p">{</span>
    <span class="k">return</span><span class="p">;</span>
  <span class="p">}</span>

  <span class="n">SetAchievement</span><span class="p">(</span><span class="n">ptr</span><span class="p">);</span>
  <span class="n">StoreStats</span><span class="p">();</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Binding</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">steam</span><span class="o">::</span><span class="n">achievement</span> <span class="n">achievement</span><span class="p">;</span>

<span class="n">lua</span><span class="p">.</span><span class="n">new_usertype</span><span class="o">&lt;</span><span class="n">steam</span><span class="o">::</span><span class="n">achievement</span><span class="o">&gt;</span><span class="p">(</span>
  <span class="s">"Achievement"</span><span class="p">,</span>
  <span class="s">"unlock"</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">steam</span><span class="o">::</span><span class="n">achievement</span><span class="o">::</span><span class="n">unlock</span>
<span class="p">);</span>

<span class="n">lua</span><span class="p">[</span><span class="s">"achievement"</span><span class="p">]</span> <span class="o">=</span> <span class="o">&amp;</span><span class="n">achievement</span><span class="p">;</span>
</code></pre></div></div>

<p>Usage</p>

<div class="language-lua highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">achievement</span><span class="p">:</span><span class="n">unlock</span><span class="p">(</span><span class="s2">"NEW_ACHIEVEMENT_1_3"</span><span class="p">)</span>
</code></pre></div></div>]]></content><author><name>Rodrigo Delduca</name><email>rodrigo@delduca.org</email></author><summary type="html"><![CDATA[As you know, this blog is more focused on sharing code snippets than on teaching, so today I’m going to show you something I recently discovered.]]></summary></entry><entry><title type="html">I’ve built an IoT device to let my family know when I’m in a meeting</title><link href="https://nullonerror.org/2025/05/11/i-have-built-an-iot-device-to-let-my-family-know-when-i-am-in-a-meeting/" rel="alternate" type="text/html" title="I’ve built an IoT device to let my family know when I’m in a meeting" /><published>2025-05-11T00:00:00+00:00</published><updated>2025-05-11T00:00:00+00:00</updated><id>https://nullonerror.org/2025/05/11/i-have-built-an-iot-device-to-let-my-family-know-when-i-am-in-a-meeting</id><content type="html" xml:base="https://nullonerror.org/2025/05/11/i-have-built-an-iot-device-to-let-my-family-know-when-i-am-in-a-meeting/"><![CDATA[<h1 id="introducing-the-iot-device-tabajara-im-in-a-meeting">Introducing the IoT device Tabajara: “I’m in a meeting.”</h1>

<p>Do you work from home, and do people in your household always show up at the worst possible moments?</p>

<p>Let me introduce the “I’m in Meeting” IoT device: it lights up at your office door whenever you turn on your webcam.</p>

<p>It consists of an ESP32 with <code class="language-plaintext highlighter-rouge">mDNS</code> connected to Wi-Fi, using the Arduino framework for simplicity. The ESP32 exposes an HTTP server that handles a PATCH request on the /camera endpoint. This endpoint receives a JSON payload with a status of “on” or “off”, and turns the LED panel red or blue accordingly.</p>

<p>For those who don’t know, mDNS (or Bonjour on Apple platforms) is a way to assign an IP address to a .local hostname for the device, so I don’t need to figure out its IP manually—just use the local domain.</p>

<p>Super convenient, right?</p>

<p>On the other side, I have a Python daemon that periodically queries Apple’s API to check if any cameras are in use, and then sends a PATCH request with “on” or “off” to http://esp32.local/camera.</p>

<p>Very simple, but quite useful.</p>

<p>See in action here <a href="https://youtu.be/c-cD_JLuCuQ">https://youtu.be/c-cD_JLuCuQ</a></p>

<p>Source code <a href="https://github.com/skhaz/onair">https://github.com/skhaz/onair</a></p>

<p>See you!</p>]]></content><author><name>Rodrigo Delduca</name><email>rodrigo@delduca.org</email></author><summary type="html"><![CDATA[Introducing the IoT device Tabajara: “I’m in a meeting.”]]></summary></entry><entry><title type="html">Building and Publishing Games to Steam Directly from GitHub Actions</title><link href="https://nullonerror.org/2025/03/23/building-and-publishing-games-to-steam-directly-from-gitHub-actions/" rel="alternate" type="text/html" title="Building and Publishing Games to Steam Directly from GitHub Actions" /><published>2025-03-23T00:00:00+00:00</published><updated>2025-03-23T00:00:00+00:00</updated><id>https://nullonerror.org/2025/03/23/building-and-publishing-games-to-steam-directly-from-gitHub-actions</id><content type="html" xml:base="https://nullonerror.org/2025/03/23/building-and-publishing-games-to-steam-directly-from-gitHub-actions/"><![CDATA[<p>I have been using GitHub Actions extensively both at work and in personal projects, as shown in this post <a href="https://nullonerror.org/2023/11/01/what-i-ve-been-automating-with-github-actions-an-automated-life/">What I’ve been automating with GitHub Actions, an automated life</a>.</p>

<p>In my free time, I’m working on a 2D hide-and-seek game, and as you might expect, I’ve automated the entire release pipeline for publishing on Steam. After a few attempts, when it finally worked, it felt like magic: all I had to do was create a new tag, and within minutes, the Steam client was downloading the update.</p>

<p>As I mentioned earlier, I have a 2D engine that, while simple, is quite comprehensive. With each new tag, I compile it in parallel for Windows, macOS, Linux, and WebAssembly. Once compilation is complete, I create a release and publish it on GitHub. <a href="https://github.com/willtobyte/carimbo/releases">Releases · willtobyte/carimbo</a></p>

<p>This way</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">name</span><span class="pi">:</span> <span class="s">Release</span>

<span class="na">on</span><span class="pi">:</span>
  <span class="na">push</span><span class="pi">:</span>
    <span class="na">tags</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">v*.*.*"</span>

<span class="na">jobs</span><span class="pi">:</span>
  <span class="na">release</span><span class="pi">:</span>
    <span class="na">runs-on</span><span class="pi">:</span> <span class="s">${{ matrix.config.os }}</span>

    <span class="na">permissions</span><span class="pi">:</span>
      <span class="na">contents</span><span class="pi">:</span> <span class="s">write</span>

    <span class="na">strategy</span><span class="pi">:</span>
      <span class="na">fail-fast</span><span class="pi">:</span> <span class="no">true</span>

      <span class="na">matrix</span><span class="pi">:</span>
        <span class="na">config</span><span class="pi">:</span>
          <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">macOS</span>
            <span class="na">os</span><span class="pi">:</span> <span class="s">macos-latest</span>
          <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Ubuntu</span>
            <span class="na">os</span><span class="pi">:</span> <span class="s">ubuntu-latest</span>
          <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">WebAssembly</span>
            <span class="na">os</span><span class="pi">:</span> <span class="s">ubuntu-latest</span>
          <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Windows</span>
            <span class="na">os</span><span class="pi">:</span> <span class="s">windows-latest</span>

    <span class="na">steps</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Checkout</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/checkout@v4</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Cache Dependencies</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/cache@v4</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">path</span><span class="pi">:</span> <span class="pi">|</span>
            <span class="s">~/.conan2/p</span>
            <span class="s">C:/Users/runneradmin/.conan2/p</span>
          <span class="na">key</span><span class="pi">:</span> <span class="s">${{ matrix.config.name }}-${{ hashFiles('**/conanfile.py') }}</span>
          <span class="na">restore-keys</span><span class="pi">:</span> <span class="pi">|</span>
            <span class="s">${{ matrix.config.name }}-</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Prepare Build Directory</span>
        <span class="na">run</span><span class="pi">:</span> <span class="s">mkdir build</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Setup Python</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/setup-python@v5</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">python-version</span><span class="pi">:</span> <span class="s2">"</span><span class="s">3.12"</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Install Conan</span>
        <span class="na">run</span><span class="pi">:</span> <span class="s">pip install conan</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Detect Conan Profile</span>
        <span class="na">run</span><span class="pi">:</span> <span class="s">conan profile detect --force</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Set Conan Center</span>
        <span class="na">run</span><span class="pi">:</span> <span class="s">conan remote update conancenter --url https://center2.conan.io</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Detect WebAssembly Conan Profile</span>
        <span class="na">if</span><span class="pi">:</span> <span class="s">matrix.config.name == 'WebAssembly'</span>
        <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">cat &gt; ~/.conan2/profiles/webassembly &lt;&lt;EOF</span>
          <span class="s">include(default)</span>

          <span class="s">[settings]</span>
          <span class="s">arch=wasm</span>
          <span class="s">os=Emscripten</span>

          <span class="s">[tool_requires]</span>
          <span class="s">*: emsdk/3.1.73</span>
          <span class="s">EOF</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Install Windows Or macOS Dependencies</span>
        <span class="na">if</span><span class="pi">:</span> <span class="s">matrix.config.name == 'Windows' || matrix.config.name == 'macOS'</span>
        <span class="na">run</span><span class="pi">:</span> <span class="s">conan install . --output-folder=build --build=missing --settings compiler.cppstd=20 --settings build_type=Release</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Install Ubuntu Dependencies</span>
        <span class="na">if</span><span class="pi">:</span> <span class="s">matrix.config.name == 'Ubuntu'</span>
        <span class="na">run</span><span class="pi">:</span> <span class="s">conan install . --output-folder=build --build=missing --settings compiler.cppstd=20 --settings build_type=Release --conf "tools.system.package_manager:mode=install" --conf "tools.system.package_manager:sudo=True"</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Install WebAssembly Dependencies</span>
        <span class="na">if</span><span class="pi">:</span> <span class="s">matrix.config.name == 'WebAssembly'</span>
        <span class="na">run</span><span class="pi">:</span> <span class="s">conan install . --output-folder=build --build=missing --profile=webassembly --settings compiler.cppstd=20 --settings build_type=Release</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Configure</span>
        <span class="na">run</span><span class="pi">:</span> <span class="s">cmake .. -DCMAKE_TOOLCHAIN_FILE="conan_toolchain.cmake" -DCMAKE_BUILD_TYPE=Release</span>
        <span class="na">working-directory</span><span class="pi">:</span> <span class="s">build</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Build</span>
        <span class="na">run</span><span class="pi">:</span> <span class="s">cmake --build . --parallel 8 --config Release --verbose</span>
        <span class="na">working-directory</span><span class="pi">:</span> <span class="s">build</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Create Artifacts Directory</span>
        <span class="na">run</span><span class="pi">:</span> <span class="s">mkdir artifacts</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Compress Artifacts</span>
        <span class="na">if</span><span class="pi">:</span> <span class="s">matrix.config.name == 'macOS'</span>
        <span class="na">working-directory</span><span class="pi">:</span> <span class="s">build</span>
        <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">chmod -R a+rwx carimbo</span>
          <span class="s">tar -cpzvf macOS.tar.gz carimbo</span>
          <span class="s">mv macOS.tar.gz ../artifacts</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Compress Artifacts</span>
        <span class="na">if</span><span class="pi">:</span> <span class="s">matrix.config.name == 'Ubuntu'</span>
        <span class="na">working-directory</span><span class="pi">:</span> <span class="s">build</span>
        <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">chmod +x carimbo</span>
          <span class="s">tar -czvf Ubuntu.tar.gz --mode='a+rwx' carimbo</span>
          <span class="s">mv Ubuntu.tar.gz ../artifacts</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Compress Artifacts</span>
        <span class="na">if</span><span class="pi">:</span> <span class="s">matrix.config.name == 'WebAssembly'</span>
        <span class="na">working-directory</span><span class="pi">:</span> <span class="s">build</span>
        <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">zip -jr WebAssembly.zip carimbo.wasm carimbo.js</span>
          <span class="s">mv WebAssembly.zip ../artifacts</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Compress Artifacts</span>
        <span class="na">if</span><span class="pi">:</span> <span class="s">matrix.config.name == 'Windows'</span>
        <span class="na">working-directory</span><span class="pi">:</span> <span class="s">build</span>
        <span class="na">shell</span><span class="pi">:</span> <span class="s">powershell</span>
        <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">Compress-Archive -LiteralPath 'Release/carimbo.exe' -DestinationPath "../artifacts/Windows.zip"</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Release</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">softprops/action-gh-release@v1</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">tag_name</span><span class="pi">:</span> <span class="s">${{ github.event.inputs.tagName }}</span>
          <span class="na">prerelease</span><span class="pi">:</span> <span class="s">${{ github.events.inputs.prerelease }}</span>
          <span class="na">files</span><span class="pi">:</span> <span class="s">artifacts/*</span>
</code></pre></div></div>

<p>Publishing on Steam is quite simple. First, you need a developer account with the correct documentation and fees paid.</p>

<p>After that, you’ll need to generate some secret keys as follows:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>steamcmd +login &lt;username&gt; &lt;password&gt; +quit
</code></pre></div></div>

<p>If you don’t have the steamcmd application installed, you’ll need to install it using:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cast <span class="nb">install</span> <span class="nt">--cask</span> steamcmd
</code></pre></div></div>

<p>Copy the contents of the authentication file:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cat</span> ~/Library/Application<span class="se">\ </span>Support/Steam/config/config.vdf | <span class="nb">base64</span> | pbcopy
</code></pre></div></div>

<p><strong>Note:</strong> You must have MFA enabled. After logging in, run the command below and copy the output into a GitHub Action variable named <code class="language-plaintext highlighter-rouge">STEAM_CONFIG_VDF</code>.</p>

<p>Also, create the variables <code class="language-plaintext highlighter-rouge">STEAM_USERNAME</code> with your username and <code class="language-plaintext highlighter-rouge">STEAM_APP_ID</code> with your game’s ID.</p>

<p>Additionally, the Action downloads the latest Carimbo release for Windows only (sorry Linux and macOS users, my time is limited). Ideally, I should pin the runtime version (the Carimbo version) using something like a runtime.txt file. Maybe I’ll implement this in the future, but for now, everything runs on the bleeding edge. :-)</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">on</span><span class="pi">:</span>
  <span class="na">push</span><span class="pi">:</span>
    <span class="na">tags</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">v*.*.*"</span>

<span class="na">jobs</span><span class="pi">:</span>
  <span class="na">publish</span><span class="pi">:</span>
    <span class="na">runs-on</span><span class="pi">:</span> <span class="s">ubuntu-latest</span>
    <span class="na">env</span><span class="pi">:</span>
      <span class="na">CARIMBO_TAG</span><span class="pi">:</span> <span class="s2">"</span><span class="s">v1.0.65"</span>

    <span class="na">permissions</span><span class="pi">:</span>
      <span class="na">contents</span><span class="pi">:</span> <span class="s">write</span>

    <span class="na">steps</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Clone the repository</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/checkout@v4</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Install 7zip</span>
        <span class="na">run</span><span class="pi">:</span> <span class="s">sudo apt install p7zip-full</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Create bundle</span>
        <span class="na">run</span><span class="pi">:</span> <span class="s">7z a -xr'!.git/*' -xr'!.git' -xr'!.*' -t7z -m0=lzma -mx=6 -mfb=64 -md=32m -ms=on bundle.7z .</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Download Carimbo runtime</span>
        <span class="na">env</span><span class="pi">:</span>
          <span class="na">GITHUB_TOKEN</span><span class="pi">:</span> <span class="s">${{ secrets.GITHUB_TOKEN }}</span>
        <span class="na">run</span><span class="pi">:</span> <span class="s">gh release download ${{ env.CARIMBO_TAG }} --repo willtobyte/carimbo --pattern "Windows.zip"</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Extract Carimbo runtime</span>
        <span class="na">run</span><span class="pi">:</span> <span class="s">7z x Windows.zip -o.</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Copy files</span>
        <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">mkdir -p output</span>
          <span class="s">mv bundle.7z output/</span>
          <span class="s">mv carimbo.exe output/</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Upload build to Steam</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">game-ci/steam-deploy@v3</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">username</span><span class="pi">:</span> <span class="s">${{ secrets.STEAM_USERNAME }}</span>
          <span class="na">configVdf</span><span class="pi">:</span> <span class="s">${{ secrets.STEAM_CONFIG_VDF }}</span>
          <span class="na">appId</span><span class="pi">:</span> <span class="s">${{ secrets.STEAM_APP_ID }}</span>
          <span class="na">rootPath</span><span class="pi">:</span> <span class="s">output</span>
          <span class="na">depot1Path</span><span class="pi">:</span> <span class="s2">"</span><span class="s">."</span>
          <span class="na">releaseBranch</span><span class="pi">:</span> <span class="s">prerelease</span>
</code></pre></div></div>

<p>Boom! If everything is correct, your game should appear in your Steam client under the list of owned games.</p>]]></content><author><name>Rodrigo Delduca</name><email>rodrigo@delduca.org</email></author><summary type="html"><![CDATA[I have been using GitHub Actions extensively both at work and in personal projects, as shown in this post What I’ve been automating with GitHub Actions, an automated life.]]></summary></entry><entry><title type="html">My First Game with Carimbo, My Homemade Engine, For my Son</title><link href="https://nullonerror.org/2024/10/08/my-first-game-with-carimbo/" rel="alternate" type="text/html" title="My First Game with Carimbo, My Homemade Engine, For my Son" /><published>2024-10-08T00:00:00+00:00</published><updated>2024-10-08T00:00:00+00:00</updated><id>https://nullonerror.org/2024/10/08/my-first-game-with-carimbo</id><content type="html" xml:base="https://nullonerror.org/2024/10/08/my-first-game-with-carimbo/"><![CDATA[<p><a href="https://youtu.be/nVRzCstyspQ"><img src="/public/2024-10-08-my-first-game-with-carimbo/megarick.webp" alt="MegaRick Game" class="center" /></a>
<a href="https://youtu.be/nVRzCstyspQ">https://youtu.be/nVRzCstyspQ</a></p>

<h3 id="tldr">TL;DR</h3>

<p>After a while, I decided to resume work on <a href="https://github.com/carimbolabs/carimbo">my game engine</a>.</p>

<p>I made a game for my son. I could have used an existing engine, but I chose to write everything from scratch because code is like pasta—it’s much better and more enjoyable when homemade.</p>

<p>This actually reminds me of when my father used to build my toys, from kites to wooden slides. Good memories. I have decided to do the same using what I know: programming.</p>

<p>You can watch it here or <a href="https://carimbo.games">play it here</a>, (runs in the browser thanks to WebAssembly), use <code class="language-plaintext highlighter-rouge">A</code> and <code class="language-plaintext highlighter-rouge">D</code> for moving around and <code class="language-plaintext highlighter-rouge">space</code> to shoot.</p>

<p>The engine was written in C++17, and the games are in Lua. The engine exposes some primitives to the Lua VM, which in turn coordinates the entire game.</p>

<p><a href="https://github.com/willtobyte/megarick">Game source code</a> and <a href="https://github.com/willtobyte/carimbo">engine source code</a>.</p>

<p>Artwork by <a href="https://www.fiverr.com/yuugenpixie">Aline Cardoso @yuugenpixie</a>.</p>

<h3 id="result">Result</h3>

<p><img src="/public/2024-10-08-my-first-game-with-carimbo/play.jpeg" alt="Henrique Playing" class="center" /></p>

<h3 id="going-deep">Going Deep</h3>

<p>The entire game is scripted in <a href="https://www.lua.org">Lua</a>.</p>

<p>First, we create the engine itself.</p>

<div class="language-lua highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">local</span> <span class="n">engine</span> <span class="o">=</span> <span class="n">EngineFactory</span><span class="p">.</span><span class="n">new</span><span class="p">()</span>
    <span class="p">:</span><span class="n">set_title</span><span class="p">(</span><span class="s2">"Mega Rick"</span><span class="p">)</span>
    <span class="p">:</span><span class="n">set_width</span><span class="p">(</span><span class="mi">1920</span><span class="p">)</span>
    <span class="p">:</span><span class="n">set_height</span><span class="p">(</span><span class="mi">1080</span><span class="p">)</span>
    <span class="p">:</span><span class="n">set_fullscreen</span><span class="p">(</span><span class="kc">false</span><span class="p">)</span>
    <span class="p">:</span><span class="n">create</span><span class="p">()</span>
</code></pre></div></div>

<p>Then we point to some resources for asset prefetching; they are loaded lazily to avoid impacting the <em>game loop</em>.</p>

<div class="language-lua highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">engine</span><span class="p">:</span><span class="n">prefetch</span><span class="p">({</span>
  <span class="s2">"blobs/bomb1.ogg"</span><span class="p">,</span>
  <span class="s2">"blobs/bomb2.ogg"</span><span class="p">,</span>
  <span class="s2">"blobs/bullet.png"</span><span class="p">,</span>
  <span class="s2">"blobs/candle.png"</span><span class="p">,</span>
  <span class="s2">"blobs/explosion.png"</span><span class="p">,</span>
  <span class="s2">"blobs/octopus.png"</span><span class="p">,</span>
  <span class="s2">"blobs/player.png"</span><span class="p">,</span>
  <span class="s2">"blobs/princess.png"</span><span class="p">,</span>
  <span class="s2">"blobs/ship.png"</span>
<span class="p">})</span>
</code></pre></div></div>

<p>We created the postal service, which is something I borrowed from languages like <em>Erlang</em>, where an entity can send messages to any other. As we’ll see below, the bullet sends a hit message when it collides with the octopus, and the octopus, upon receiving the hit, decrements its own health.</p>

<p>We also obtain the SoundManager to play sounds and spawn the main entities.</p>

<div class="language-lua highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">local</span> <span class="n">postal</span> <span class="o">=</span> <span class="n">PostalService</span><span class="p">.</span><span class="n">new</span><span class="p">()</span>
<span class="kd">local</span> <span class="n">soundmanager</span> <span class="o">=</span> <span class="n">engine</span><span class="p">:</span><span class="n">soundmanager</span><span class="p">()</span>
<span class="kd">local</span> <span class="n">octopus</span> <span class="o">=</span> <span class="n">engine</span><span class="p">:</span><span class="n">spawn</span><span class="p">(</span><span class="s2">"octopus"</span><span class="p">)</span>
<span class="kd">local</span> <span class="n">player</span> <span class="o">=</span> <span class="n">engine</span><span class="p">:</span><span class="n">spawn</span><span class="p">(</span><span class="s2">"player"</span><span class="p">)</span>
<span class="kd">local</span> <span class="n">princess</span> <span class="o">=</span> <span class="n">engine</span><span class="p">:</span><span class="n">spawn</span><span class="p">(</span><span class="s2">"princess"</span><span class="p">)</span>
<span class="kd">local</span> <span class="n">candle1</span> <span class="o">=</span> <span class="n">engine</span><span class="p">:</span><span class="n">spawn</span><span class="p">(</span><span class="s2">"candle"</span><span class="p">)</span>
<span class="kd">local</span> <span class="n">candle2</span> <span class="o">=</span> <span class="n">engine</span><span class="p">:</span><span class="n">spawn</span><span class="p">(</span><span class="s2">"candle"</span><span class="p">)</span>
</code></pre></div></div>

<p>It’s not a triple-A title, but I’m very careful with resource management. A widely known technique is to create an object pool that you can reuse. In the code below, we limit it to a maximum of 3 bullets present on the screen.</p>

<p>The <code class="language-plaintext highlighter-rouge">on_update</code> method is a callback called each loop in the engine. In the case of the bullet, I check if it has approached the green octopus. If so, I send a message to it indicating that it received a hit.</p>

<div class="language-lua highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">for</span> <span class="n">_</span> <span class="o">=</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">3</span> <span class="k">do</span>
    <span class="kd">local</span> <span class="n">bullet</span> <span class="o">=</span> <span class="n">entitymanager</span><span class="p">:</span><span class="n">spawn</span><span class="p">(</span><span class="s2">"bullet"</span><span class="p">)</span>
    <span class="n">bullet</span><span class="p">.</span><span class="n">placement</span><span class="p">:</span><span class="n">set</span><span class="p">(</span><span class="o">-</span><span class="mi">128</span><span class="p">,</span> <span class="o">-</span><span class="mi">128</span><span class="p">)</span>
    <span class="n">bullet</span><span class="p">:</span><span class="n">on_collision</span><span class="p">(</span><span class="s2">"octopus"</span><span class="p">,</span> <span class="k">function</span><span class="p">(</span><span class="n">self</span><span class="p">)</span>
      <span class="n">self</span><span class="p">.</span><span class="n">action</span><span class="p">:</span><span class="n">unset</span><span class="p">()</span>
      <span class="n">self</span><span class="p">.</span><span class="n">placement</span><span class="p">:</span><span class="n">set</span><span class="p">(</span><span class="o">-</span><span class="mi">128</span><span class="p">,</span> <span class="o">-</span><span class="mi">128</span><span class="p">)</span>
      <span class="n">postalservice</span><span class="p">:</span><span class="n">post</span><span class="p">(</span><span class="n">Mail</span><span class="p">.</span><span class="n">new</span><span class="p">(</span><span class="n">octopus</span><span class="p">,</span> <span class="s2">"bullet"</span><span class="p">,</span> <span class="s2">"hit"</span><span class="p">))</span>
      <span class="nb">table.insert</span><span class="p">(</span><span class="n">bullet_pool</span><span class="p">,</span> <span class="n">self</span><span class="p">)</span>
    <span class="k">end</span><span class="p">)</span>
    <span class="nb">table.insert</span><span class="p">(</span><span class="n">bullet_pool</span><span class="p">,</span> <span class="n">bullet</span><span class="p">)</span>
  <span class="k">end</span>
</code></pre></div></div>

<p>On the octopus’s side, when it receives a “hit” message, the entity triggers an explosion animation, switches to attack mode, and decreases its health by 1. If it reaches 0, it changes the animation to “dead”.</p>

<div class="language-lua highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">octopus</span> <span class="o">=</span> <span class="n">entitymanager</span><span class="p">:</span><span class="n">spawn</span><span class="p">(</span><span class="s2">"octopus"</span><span class="p">)</span>
<span class="n">octopus</span><span class="p">.</span><span class="n">kv</span><span class="p">:</span><span class="n">set</span><span class="p">(</span><span class="s2">"life"</span><span class="p">,</span> <span class="mi">16</span><span class="p">)</span>
<span class="n">octopus</span><span class="p">.</span><span class="n">placement</span><span class="p">:</span><span class="n">set</span><span class="p">(</span><span class="mi">1200</span><span class="p">,</span> <span class="mi">622</span><span class="p">)</span>
<span class="n">octopus</span><span class="p">.</span><span class="n">action</span><span class="p">:</span><span class="n">set</span><span class="p">(</span><span class="s2">"idle"</span><span class="p">)</span>
<span class="n">octopus</span><span class="p">:</span><span class="n">on_mail</span><span class="p">(</span><span class="k">function</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">message</span><span class="p">)</span>
  <span class="kd">local</span> <span class="n">behavior</span> <span class="o">=</span> <span class="n">behaviors</span><span class="p">[</span><span class="n">message</span><span class="p">]</span>
  <span class="k">if</span> <span class="n">behavior</span> <span class="k">then</span>
    <span class="n">behavior</span><span class="p">(</span><span class="n">self</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span><span class="p">)</span>
<span class="n">octopus</span><span class="p">.</span><span class="n">kv</span><span class="p">:</span><span class="n">subscribe</span><span class="p">(</span><span class="s2">"life"</span><span class="p">,</span> <span class="k">function</span><span class="p">(</span><span class="n">value</span><span class="p">)</span>
  <span class="n">vitality</span><span class="p">:</span><span class="n">set</span><span class="p">(</span><span class="nb">string.format</span><span class="p">(</span><span class="s2">"%02d-"</span><span class="p">,</span> <span class="nb">math.max</span><span class="p">(</span><span class="n">value</span><span class="p">,</span> <span class="mi">0</span><span class="p">)))</span>

  <span class="k">if</span> <span class="n">value</span> <span class="o">&lt;=</span> <span class="mi">0</span> <span class="k">then</span>
    <span class="n">octopus</span><span class="p">.</span><span class="n">action</span><span class="p">:</span><span class="n">set</span><span class="p">(</span><span class="s2">"dead"</span><span class="p">)</span>
    <span class="k">if</span> <span class="ow">not</span> <span class="n">timer</span> <span class="k">then</span>
      <span class="n">timemanager</span><span class="p">:</span><span class="n">singleshot</span><span class="p">(</span><span class="mi">3000</span><span class="p">,</span> <span class="k">function</span><span class="p">()</span>
        <span class="kd">local</span> <span class="k">function</span> <span class="nf">destroy</span><span class="p">(</span><span class="n">pool</span><span class="p">)</span>
          <span class="k">for</span> <span class="n">i</span> <span class="o">=</span> <span class="o">#</span><span class="n">pool</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="o">-</span><span class="mi">1</span> <span class="k">do</span>
            <span class="n">entitymanager</span><span class="p">:</span><span class="n">destroy</span><span class="p">(</span><span class="n">pool</span><span class="p">[</span><span class="n">i</span><span class="p">])</span>
            <span class="nb">table.remove</span><span class="p">(</span><span class="n">pool</span><span class="p">,</span> <span class="n">i</span><span class="p">)</span>
            <span class="n">pool</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">=</span> <span class="kc">nil</span>
          <span class="k">end</span>
        <span class="k">end</span>

        <span class="n">destroy</span><span class="p">(</span><span class="n">bullet_pool</span><span class="p">)</span>
        <span class="n">destroy</span><span class="p">(</span><span class="n">explosion_pool</span><span class="p">)</span>
        <span class="n">destroy</span><span class="p">(</span><span class="n">jet_pool</span><span class="p">)</span>

        <span class="n">entitymanager</span><span class="p">:</span><span class="n">destroy</span><span class="p">(</span><span class="n">octopus</span><span class="p">)</span>
        <span class="n">octopus</span> <span class="o">=</span> <span class="kc">nil</span>

        <span class="n">entitymanager</span><span class="p">:</span><span class="n">destroy</span><span class="p">(</span><span class="n">player</span><span class="p">)</span>
        <span class="n">player</span> <span class="o">=</span> <span class="kc">nil</span>

        <span class="n">entitymanager</span><span class="p">:</span><span class="n">destroy</span><span class="p">(</span><span class="n">princess</span><span class="p">)</span>
        <span class="n">princess</span> <span class="o">=</span> <span class="kc">nil</span>

        <span class="n">entitymanager</span><span class="p">:</span><span class="n">destroy</span><span class="p">(</span><span class="n">candle1</span><span class="p">)</span>
        <span class="n">candle1</span> <span class="o">=</span> <span class="kc">nil</span>

        <span class="n">entitymanager</span><span class="p">:</span><span class="n">destroy</span><span class="p">(</span><span class="n">candle2</span><span class="p">)</span>
        <span class="n">candle2</span> <span class="o">=</span> <span class="kc">nil</span>

        <span class="n">entitymanager</span><span class="p">:</span><span class="n">destroy</span><span class="p">(</span><span class="n">floor</span><span class="p">)</span>
        <span class="n">floor</span> <span class="o">=</span> <span class="kc">nil</span>

        <span class="n">overlay</span><span class="p">:</span><span class="n">destroy</span><span class="p">(</span><span class="n">vitality</span><span class="p">)</span>
        <span class="n">vitality</span> <span class="o">=</span> <span class="kc">nil</span>

        <span class="n">overlay</span><span class="p">:</span><span class="n">destroy</span><span class="p">(</span><span class="n">online</span><span class="p">)</span>
        <span class="n">online</span> <span class="o">=</span> <span class="kc">nil</span>

        <span class="n">scenemanager</span><span class="p">:</span><span class="n">set</span><span class="p">(</span><span class="s2">"gameover"</span><span class="p">)</span>

        <span class="nb">collectgarbage</span><span class="p">(</span><span class="s2">"collect"</span><span class="p">)</span>

        <span class="n">resourcemanager</span><span class="p">:</span><span class="n">flush</span><span class="p">()</span>
      <span class="k">end</span><span class="p">)</span>
      <span class="n">timer</span> <span class="o">=</span> <span class="kc">true</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span><span class="p">)</span>
<span class="n">octopus</span><span class="p">:</span><span class="n">on_animationfinished</span><span class="p">(</span><span class="k">function</span><span class="p">(</span><span class="n">self</span><span class="p">)</span>
  <span class="n">self</span><span class="p">.</span><span class="n">action</span><span class="p">:</span><span class="n">set</span><span class="p">(</span><span class="s2">"idle"</span><span class="p">)</span>
<span class="k">end</span><span class="p">)</span>
</code></pre></div></div>

<p>And finally, the player’s <code class="language-plaintext highlighter-rouge">on_update</code>, where I apply the velocity if any movement keys are pressed, or set the animation to idle if none are pressed.</p>

<p>We also have the projectile firing, where the fire function is called, taking an instance of a bullet from the pool, placing it in a semi-random position, and applying velocity.</p>

<div class="language-lua highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">function</span> <span class="nf">loop</span><span class="p">()</span>
  <span class="k">if</span> <span class="ow">not</span> <span class="n">player</span> <span class="k">then</span>
    <span class="k">return</span>
  <span class="k">end</span>

  <span class="n">player</span><span class="p">.</span><span class="n">velocity</span><span class="p">.</span><span class="n">x</span> <span class="o">=</span> <span class="mi">0</span>

  <span class="k">if</span> <span class="n">statemanager</span><span class="p">:</span><span class="n">is_keydown</span><span class="p">(</span><span class="n">KeyEvent</span><span class="p">.</span><span class="n">left</span><span class="p">)</span> <span class="k">then</span>
    <span class="n">player</span><span class="p">.</span><span class="n">reflection</span><span class="p">:</span><span class="n">set</span><span class="p">(</span><span class="n">Reflection</span><span class="p">.</span><span class="n">horizontal</span><span class="p">)</span>
    <span class="n">player</span><span class="p">.</span><span class="n">velocity</span><span class="p">.</span><span class="n">x</span> <span class="o">=</span> <span class="o">-</span><span class="mi">360</span>
  <span class="k">elseif</span> <span class="n">statemanager</span><span class="p">:</span><span class="n">is_keydown</span><span class="p">(</span><span class="n">KeyEvent</span><span class="p">.</span><span class="n">right</span><span class="p">)</span> <span class="k">then</span>
    <span class="n">player</span><span class="p">.</span><span class="n">reflection</span><span class="p">:</span><span class="n">unset</span><span class="p">()</span>
    <span class="n">player</span><span class="p">.</span><span class="n">velocity</span><span class="p">.</span><span class="n">x</span> <span class="o">=</span> <span class="mi">360</span>
  <span class="k">end</span>

  <span class="n">player</span><span class="p">.</span><span class="n">action</span><span class="p">:</span><span class="n">set</span><span class="p">(</span><span class="n">player</span><span class="p">.</span><span class="n">velocity</span><span class="p">.</span><span class="n">x</span> <span class="o">~=</span> <span class="mi">0</span> <span class="ow">and</span> <span class="s2">"run"</span> <span class="ow">or</span> <span class="s2">"idle"</span><span class="p">)</span>

  <span class="k">if</span> <span class="n">statemanager</span><span class="p">:</span><span class="n">is_keydown</span><span class="p">(</span><span class="n">KeyEvent</span><span class="p">.</span><span class="n">space</span><span class="p">)</span> <span class="k">then</span>
    <span class="k">if</span> <span class="ow">not</span> <span class="n">key_states</span><span class="p">[</span><span class="n">KeyEvent</span><span class="p">.</span><span class="n">space</span><span class="p">]</span> <span class="k">then</span>
      <span class="n">key_states</span><span class="p">[</span><span class="n">KeyEvent</span><span class="p">.</span><span class="n">space</span><span class="p">]</span> <span class="o">=</span> <span class="kc">true</span>

      <span class="c1">-- player.velocity.y = -360</span>

      <span class="k">if</span> <span class="n">octopus</span><span class="p">.</span><span class="n">kv</span><span class="p">:</span><span class="n">get</span><span class="p">(</span><span class="s2">"life"</span><span class="p">)</span> <span class="o">&lt;=</span> <span class="mi">0</span> <span class="k">then</span>
        <span class="k">return</span>
      <span class="k">end</span>

      <span class="k">if</span> <span class="o">#</span><span class="n">bullet_pool</span> <span class="o">&gt;</span> <span class="mi">0</span> <span class="k">then</span>
        <span class="kd">local</span> <span class="n">bullet</span> <span class="o">=</span> <span class="nb">table.remove</span><span class="p">(</span><span class="n">bullet_pool</span><span class="p">)</span>
        <span class="kd">local</span> <span class="n">x</span> <span class="o">=</span> <span class="p">(</span><span class="n">player</span><span class="p">.</span><span class="n">x</span> <span class="o">+</span> <span class="n">player</span><span class="p">.</span><span class="n">size</span><span class="p">.</span><span class="n">width</span><span class="p">)</span> <span class="o">+</span> <span class="mi">100</span>
        <span class="kd">local</span> <span class="n">y</span> <span class="o">=</span> <span class="n">player</span><span class="p">.</span><span class="n">y</span> <span class="o">+</span> <span class="mi">10</span>
        <span class="kd">local</span> <span class="n">offset_y</span> <span class="o">=</span> <span class="p">(</span><span class="nb">math.random</span><span class="p">(</span><span class="o">-</span><span class="mi">2</span><span class="p">,</span> <span class="mi">2</span><span class="p">))</span> <span class="o">*</span> <span class="mi">30</span>

        <span class="n">bullet</span><span class="p">.</span><span class="n">placement</span><span class="p">:</span><span class="n">set</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span> <span class="o">+</span> <span class="n">offset_y</span><span class="p">)</span>
        <span class="n">bullet</span><span class="p">.</span><span class="n">action</span><span class="p">:</span><span class="n">set</span><span class="p">(</span><span class="s2">"default"</span><span class="p">)</span>
        <span class="n">bullet</span><span class="p">.</span><span class="n">velocity</span><span class="p">.</span><span class="n">x</span> <span class="o">=</span> <span class="mi">800</span>

        <span class="kd">local</span> <span class="n">sound</span> <span class="o">=</span> <span class="s2">"bomb"</span> <span class="o">..</span> <span class="nb">math.random</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">)</span>
        <span class="n">soundmanager</span><span class="p">:</span><span class="n">play</span><span class="p">(</span><span class="n">sound</span><span class="p">)</span>
      <span class="k">end</span>
    <span class="k">end</span>
  <span class="k">else</span>
    <span class="n">key_states</span><span class="p">[</span><span class="n">KeyEvent</span><span class="p">.</span><span class="n">space</span><span class="p">]</span> <span class="o">=</span> <span class="kc">false</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<h3 id="in-the-end">In The End</h3>

<p>In the end, he requested some <em>minor</em> tweaks, such as the projectile being the princess sprite, the target being the player, and the entity that shoots being the green octopus. 🤷‍♂️</p>

<p><img src="/public/2024-10-08-my-first-game-with-carimbo/tweaks.jpeg" alt="Tweaks" class="center" /></p>]]></content><author><name>Rodrigo Delduca</name><email>rodrigo@delduca.org</email></author><summary type="html"><![CDATA[https://youtu.be/nVRzCstyspQ]]></summary></entry><entry><title type="html">How I saved a few dozen dollars by migrating my personal projects from the cloud to a Raspberry Pi</title><link href="https://nullonerror.org/2024/06/05/how-i-saved-a-few-dozen-dollars-by-migrating-my-personal-projects-from-the-cloud-to-a-raspberry-pi/" rel="alternate" type="text/html" title="How I saved a few dozen dollars by migrating my personal projects from the cloud to a Raspberry Pi" /><published>2024-06-05T00:00:00+00:00</published><updated>2024-06-05T00:00:00+00:00</updated><id>https://nullonerror.org/2024/06/05/how-i-saved-a-few-dozen-dollars-by-migrating-my-personal-projects-from-the-cloud-to-a-raspberry-pi</id><content type="html" xml:base="https://nullonerror.org/2024/06/05/how-i-saved-a-few-dozen-dollars-by-migrating-my-personal-projects-from-the-cloud-to-a-raspberry-pi/"><![CDATA[<h3 id="intro">Intro</h3>

<p>Since the first URL shorteners emerged, that’s always drawn me in for some reason, perhaps because I figured out on my own that they encoded the primary key in base36 (maybe).</p>

<p>So around ~2010, I decided to make my own. It was called “encurta.ae” (make it short there in Portuguese), and it was based on AppEngine. Even back then, I was quite fond of serverless and such.</p>

<p>It worked great locally, but when I deployed it, the datastore IDs were too large, causing the URLs to be too long.</p>

<p>Fast-forward to nowadays, I’ve decided to bring this project to life again. This time with a twist: when shortening the URL, the shortener would take a screenshot of the website and embed Open Graph tags in the redirect URL. This way, when shared on a social network, the link would display a title, description, and thumbnail that accurately represent what the user is about to open.</p>

<h3 id="stack">Stack</h3>

<p>Typically, I use serverless for my personal projects because they scale to zero, thus eliminating costs when not in use. However, after working with serverless for many years, I’ve been wanting to experiment with a more grounded and down-to-earth approach to development and deployment.</p>

<ul>
  <li>I opted to use <a href="https://go.dev/">Go</a>, with several <a href="https://en.wikipedia.org/wiki/Coroutine">goroutines</a> performing tasks in parallel, and a purely written work queue mechanism.</li>
  <li><a href="https://playwright.dev/">Playwright</a> &amp; Chromium for automation and screenshot.</li>
  <li><a href="https://sqlite.org/">SQLite</a> for the database (it’s simple)</li>
  <li><a href="https://www.backblaze.com/">Backblaze</a> for storage</li>
  <li><a href="https://bunny.net/">BunnyCDN</a> for cotent delivery</li>
  <li><a href="https://www.papertrail.com/">Papertrail</a> for logging</li>
</ul>

<p>Deployment is done using Docker Compose, via SSH using the DOCKER_HOST environment variable pointing directly to a <a href="https://www.raspberrypi.com/">Raspberry Pi</a> that I had bought and never used before. Now it saves me $5 per month, and I can keep a limited number of projects running on it.</p>

<p>And then you might ask: How do you expose it to the internet? I use <a href="https://www.cloudflare.com/products/tunnel/">Cloudflare Tunnel</a>; the setup is simple and creates a direct connection between the Raspberry Pi and the nearest <a href="https://en.wikipedia.org/wiki/Point_of_presence">Point of Presence</a>.</p>

<p>This type of hosting is extremely advantageous because the server’s IP and/or ports are never revealed; it stays behind my firewall. Everything goes through Cloudflare.</p>

<p>I have more than one Docker Compose file, and that’s what’s coolest. Locally, I run one, for deployment I instruct the Compose to read two others for logging and tunneling.</p>

<p><code class="language-plaintext highlighter-rouge">docker-compose.yaml</code></p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">services</span><span class="pi">:</span>
  <span class="na">app</span><span class="pi">:</span>
    <span class="na">build</span><span class="pi">:</span> <span class="s">.</span>
    <span class="na">env_file</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">.env</span>
    <span class="na">ports</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">8000:8000"</span>
    <span class="na">restart</span><span class="pi">:</span> <span class="s">unless-stopped</span>
    <span class="na">volumes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">data:/data</span>
    <span class="na">tmpfs</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">/tmp</span>
<span class="na">volumes</span><span class="pi">:</span>
  <span class="na">data</span><span class="pi">:</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">docker-compose.logging.yaml</code></p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">services</span><span class="pi">:</span>
  <span class="na">app</span><span class="pi">:</span>
    <span class="na">logging</span><span class="pi">:</span>
      <span class="na">driver</span><span class="pi">:</span> <span class="s">syslog</span>
      <span class="na">options</span><span class="pi">:</span>
        <span class="na">syslog-address</span><span class="pi">:</span> <span class="s2">"</span><span class="s">udp://logs2.papertrailapp.com:XXX"</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">docker-compose.cloudflare.yaml</code></p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">services</span><span class="pi">:</span>
  <span class="na">tunnel</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">cloudflare/cloudflared</span>
    <span class="na">restart</span><span class="pi">:</span> <span class="s">unless-stopped</span>
    <span class="na">command</span><span class="pi">:</span> <span class="s">tunnel run</span>
    <span class="na">environment</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">TUNNEL_TOKEN=yourtoken</span>
</code></pre></div></div>

<p>Then for deploying</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">DOCKER_HOST</span><span class="o">=</span>ssh://pi@192.168.0.10 docker compose <span class="nt">--file</span> docker-compose.yaml <span class="nt">--file</span> docker-compose.logging.yaml <span class="nt">--file</span> docker-compose.cloudflare.yaml up <span class="nt">--build</span> <span class="nt">--detach</span>
</code></pre></div></div>

<p>Example of the “worker queue”</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">package</span> <span class="n">functions</span>

<span class="k">import</span> <span class="p">(</span>
	<span class="s">"context"</span>
	<span class="s">"database/sql"</span>
	<span class="s">"fmt"</span>
	<span class="s">"os"</span>
	<span class="s">"os/exec"</span>
	<span class="s">"path/filepath"</span>
	<span class="s">"sync"</span>
	<span class="s">"time"</span>

	<span class="n">google</span> <span class="s">"cloud.google.com/go/vision/apiv1"</span>
	<span class="n">visionpb</span> <span class="s">"cloud.google.com/go/vision/v2/apiv1/visionpb"</span>
	<span class="s">"github.com/martinlindhe/base36"</span>
	<span class="s">"github.com/minio/minio-go/v7"</span>
	<span class="s">"github.com/minio/minio-go/v7/pkg/credentials"</span>
	<span class="s">"github.com/playwright-community/playwright-go"</span>
	<span class="s">"go.uber.org/zap"</span>
	<span class="s">"google.golang.org/api/option"</span>
	<span class="n">log</span> <span class="s">"skhaz.dev/urlshortnen/logging"</span>
<span class="p">)</span>

<span class="k">var</span> <span class="p">(</span>
	<span class="n">accessKeyID</span>     <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">Getenv</span><span class="p">(</span><span class="s">"BACKBLAZE_ACCESS_ID"</span><span class="p">)</span>
	<span class="n">secretAccessKey</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">Getenv</span><span class="p">(</span><span class="s">"BACKBLAZE_APPLICATION_KEY"</span><span class="p">)</span>
	<span class="n">bucket</span>          <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">Getenv</span><span class="p">(</span><span class="s">"BACKBLAZE_BUCKET"</span><span class="p">)</span>
	<span class="n">endpoint</span>        <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">Getenv</span><span class="p">(</span><span class="s">"BACKBLAZE_ENDPOINT"</span><span class="p">)</span>
	<span class="n">useSSL</span>          <span class="o">=</span> <span class="no">true</span>
	<span class="n">extension</span>       <span class="o">=</span> <span class="s">"webp"</span>
	<span class="n">mimetype</span>        <span class="o">=</span> <span class="s">"image/webp"</span>
	<span class="n">quality</span>         <span class="o">=</span> <span class="s">"50"</span>
<span class="p">)</span>

<span class="k">type</span> <span class="n">WorkerFunctions</span> <span class="k">struct</span> <span class="p">{</span>
	<span class="n">db</span>      <span class="o">*</span><span class="n">sql</span><span class="o">.</span><span class="n">DB</span>
	<span class="n">vision</span>  <span class="o">*</span><span class="n">google</span><span class="o">.</span><span class="n">ImageAnnotatorClient</span>
	<span class="n">mc</span>      <span class="o">*</span><span class="n">minio</span><span class="o">.</span><span class="n">Client</span>
	<span class="n">browser</span> <span class="n">playwright</span><span class="o">.</span><span class="n">BrowserContext</span>
<span class="p">}</span>

<span class="k">func</span> <span class="n">Worker</span><span class="p">(</span><span class="n">db</span> <span class="o">*</span><span class="n">sql</span><span class="o">.</span><span class="n">DB</span><span class="p">)</span> <span class="p">{</span>
	<span class="k">defer</span> <span class="k">func</span><span class="p">()</span> <span class="p">{</span>
		<span class="k">if</span> <span class="n">r</span> <span class="o">:=</span> <span class="nb">recover</span><span class="p">();</span> <span class="n">r</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
			<span class="n">log</span><span class="o">.</span><span class="n">Error</span><span class="p">(</span><span class="s">"worker panic"</span><span class="p">,</span> <span class="n">zap</span><span class="o">.</span><span class="n">Any</span><span class="p">(</span><span class="s">"error"</span><span class="p">,</span> <span class="n">r</span><span class="p">))</span>
			<span class="n">time</span><span class="o">.</span><span class="n">Sleep</span><span class="p">(</span><span class="n">time</span><span class="o">.</span><span class="n">Second</span> <span class="o">*</span> <span class="m">10</span><span class="p">)</span>
			<span class="k">go</span> <span class="n">Worker</span><span class="p">(</span><span class="n">db</span><span class="p">)</span>
		<span class="p">}</span>
	<span class="p">}()</span>

	<span class="n">ctx</span> <span class="o">:=</span> <span class="n">context</span><span class="o">.</span><span class="n">Background</span><span class="p">()</span>

	<span class="n">vision</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">google</span><span class="o">.</span><span class="n">NewImageAnnotatorClient</span><span class="p">(</span><span class="n">ctx</span><span class="p">,</span> <span class="n">option</span><span class="o">.</span><span class="n">WithCredentialsJSON</span><span class="p">([]</span><span class="kt">byte</span><span class="p">(</span><span class="n">os</span><span class="o">.</span><span class="n">Getenv</span><span class="p">(</span><span class="s">"GOOGLE_CREDENTIALS"</span><span class="p">))))</span>
	<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
		<span class="n">log</span><span class="o">.</span><span class="n">Error</span><span class="p">(</span><span class="s">"failed to create vision client"</span><span class="p">,</span> <span class="n">zap</span><span class="o">.</span><span class="n">Error</span><span class="p">(</span><span class="n">err</span><span class="p">))</span>
		<span class="k">return</span>
	<span class="p">}</span>
	<span class="k">defer</span> <span class="n">vision</span><span class="o">.</span><span class="n">Close</span><span class="p">()</span>

	<span class="n">mc</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">minio</span><span class="o">.</span><span class="n">New</span><span class="p">(</span><span class="n">endpoint</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">minio</span><span class="o">.</span><span class="n">Options</span><span class="p">{</span>
		<span class="n">Creds</span><span class="o">:</span>  <span class="n">credentials</span><span class="o">.</span><span class="n">NewStaticV4</span><span class="p">(</span><span class="n">accessKeyID</span><span class="p">,</span> <span class="n">secretAccessKey</span><span class="p">,</span> <span class="s">""</span><span class="p">),</span>
		<span class="n">Secure</span><span class="o">:</span> <span class="n">useSSL</span><span class="p">,</span>
	<span class="p">})</span>
	<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
		<span class="n">log</span><span class="o">.</span><span class="n">Error</span><span class="p">(</span><span class="s">"failed to create minio client"</span><span class="p">,</span> <span class="n">zap</span><span class="o">.</span><span class="n">Error</span><span class="p">(</span><span class="n">err</span><span class="p">))</span>
		<span class="k">return</span>
	<span class="p">}</span>

	<span class="n">pw</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">playwright</span><span class="o">.</span><span class="n">Run</span><span class="p">()</span>
	<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
		<span class="n">log</span><span class="o">.</span><span class="n">Error</span><span class="p">(</span><span class="s">"failed to launch playwright"</span><span class="p">,</span> <span class="n">zap</span><span class="o">.</span><span class="n">Error</span><span class="p">(</span><span class="n">err</span><span class="p">))</span>
		<span class="k">return</span>
	<span class="p">}</span>
	<span class="c">//nolint:golint,errcheck</span>
	<span class="k">defer</span> <span class="n">pw</span><span class="o">.</span><span class="n">Stop</span><span class="p">()</span>

	<span class="n">userDataDir</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">os</span><span class="o">.</span><span class="n">MkdirTemp</span><span class="p">(</span><span class="s">""</span><span class="p">,</span> <span class="s">"chromium"</span><span class="p">)</span>
	<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
		<span class="n">log</span><span class="o">.</span><span class="n">Error</span><span class="p">(</span><span class="s">"failed to create temporary directory"</span><span class="p">,</span> <span class="n">zap</span><span class="o">.</span><span class="n">Error</span><span class="p">(</span><span class="n">err</span><span class="p">))</span>
		<span class="k">return</span>
	<span class="p">}</span>
	<span class="k">defer</span> <span class="n">os</span><span class="o">.</span><span class="n">RemoveAll</span><span class="p">(</span><span class="n">userDataDir</span><span class="p">)</span>

	<span class="n">browser</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">pw</span><span class="o">.</span><span class="n">Chromium</span><span class="o">.</span><span class="n">LaunchPersistentContext</span><span class="p">(</span><span class="n">userDataDir</span><span class="p">,</span> <span class="n">playwright</span><span class="o">.</span><span class="n">BrowserTypeLaunchPersistentContextOptions</span><span class="p">{</span>
		<span class="n">Args</span><span class="o">:</span> <span class="p">[]</span><span class="kt">string</span><span class="p">{</span>
			<span class="s">"--headless=new"</span><span class="p">,</span>
			<span class="s">"--no-zygote"</span><span class="p">,</span>
			<span class="s">"--no-sandbox"</span><span class="p">,</span>
			<span class="s">"--disable-gpu"</span><span class="p">,</span>
			<span class="s">"--hide-scrollbars"</span><span class="p">,</span>
			<span class="s">"--disable-setuid-sandbox"</span><span class="p">,</span>
			<span class="s">"--disable-dev-shm-usage"</span><span class="p">,</span>
			<span class="s">"--disable-extensions-except=/opt/extensions/ublock,/opt/extensions/isdncac"</span><span class="p">,</span>
			<span class="s">"--load-extension=/opt/extensions/ublock,/opt/extensions/isdncac"</span><span class="p">,</span>
		<span class="p">},</span>
		<span class="n">DeviceScaleFactor</span><span class="o">:</span> <span class="n">playwright</span><span class="o">.</span><span class="n">Float</span><span class="p">(</span><span class="m">4.0</span><span class="p">),</span>
		<span class="n">Headless</span><span class="o">:</span>          <span class="n">playwright</span><span class="o">.</span><span class="n">Bool</span><span class="p">(</span><span class="no">false</span><span class="p">),</span>
		<span class="n">Viewport</span><span class="o">:</span> <span class="o">&amp;</span><span class="n">playwright</span><span class="o">.</span><span class="n">Size</span><span class="p">{</span>
			<span class="n">Width</span><span class="o">:</span>  <span class="m">1200</span><span class="p">,</span>
			<span class="n">Height</span><span class="o">:</span> <span class="m">630</span><span class="p">,</span>
		<span class="p">},</span>
		<span class="n">UserAgent</span><span class="o">:</span> <span class="n">playwright</span><span class="o">.</span><span class="n">String</span><span class="p">(</span><span class="s">"Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; Googlebot/2.1; +http://www.google.com/bot.html) Chrome/125.0.0.0 Safari/537.36"</span><span class="p">),</span>
	<span class="p">})</span>
	<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
		<span class="n">log</span><span class="o">.</span><span class="n">Error</span><span class="p">(</span><span class="s">"failed to launch chromium browser"</span><span class="p">,</span> <span class="n">zap</span><span class="o">.</span><span class="n">Error</span><span class="p">(</span><span class="n">err</span><span class="p">))</span>
		<span class="k">return</span>
	<span class="p">}</span>
	<span class="k">defer</span> <span class="n">browser</span><span class="o">.</span><span class="n">Close</span><span class="p">()</span>

	<span class="n">wf</span> <span class="o">:=</span> <span class="n">WorkerFunctions</span><span class="p">{</span>
		<span class="n">db</span><span class="o">:</span>      <span class="n">db</span><span class="p">,</span>
		<span class="n">vision</span><span class="o">:</span>  <span class="n">vision</span><span class="p">,</span>
		<span class="n">mc</span><span class="o">:</span>      <span class="n">mc</span><span class="p">,</span>
		<span class="n">browser</span><span class="o">:</span> <span class="n">browser</span><span class="p">,</span>
	<span class="p">}</span>

	<span class="k">for</span> <span class="p">{</span>
		<span class="n">start</span> <span class="o">:=</span> <span class="n">time</span><span class="o">.</span><span class="n">Now</span><span class="p">()</span>

		<span class="k">func</span><span class="p">()</span> <span class="p">{</span>
			<span class="k">var</span> <span class="p">(</span>
				<span class="n">wg</span>   <span class="n">sync</span><span class="o">.</span><span class="n">WaitGroup</span>
				<span class="n">rows</span> <span class="o">*</span><span class="n">sql</span><span class="o">.</span><span class="n">Rows</span>
				<span class="n">err</span>  <span class="kt">error</span>
			<span class="p">)</span>

			<span class="n">rows</span><span class="p">,</span> <span class="n">err</span> <span class="o">=</span> <span class="n">db</span><span class="o">.</span><span class="n">Query</span><span class="p">(</span><span class="s">"SELECT id, url FROM data WHERE ready = 0 ORDER BY created_at LIMIT 6"</span><span class="p">)</span>
			<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
				<span class="n">log</span><span class="o">.</span><span class="n">Error</span><span class="p">(</span><span class="s">"error executing query"</span><span class="p">,</span> <span class="n">zap</span><span class="o">.</span><span class="n">Error</span><span class="p">(</span><span class="n">err</span><span class="p">))</span>
				<span class="k">return</span>
			<span class="p">}</span>
			<span class="k">defer</span> <span class="n">rows</span><span class="o">.</span><span class="n">Close</span><span class="p">()</span>

			<span class="k">for</span> <span class="n">rows</span><span class="o">.</span><span class="n">Next</span><span class="p">()</span> <span class="p">{</span>
				<span class="k">var</span> <span class="n">id</span> <span class="kt">int64</span>
				<span class="k">var</span> <span class="n">url</span> <span class="kt">string</span>
				<span class="k">if</span> <span class="n">err</span> <span class="o">=</span> <span class="n">rows</span><span class="o">.</span><span class="n">Scan</span><span class="p">(</span><span class="o">&amp;</span><span class="n">id</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">url</span><span class="p">);</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
					<span class="n">log</span><span class="o">.</span><span class="n">Error</span><span class="p">(</span><span class="s">"error scanning row"</span><span class="p">,</span> <span class="n">zap</span><span class="o">.</span><span class="n">Error</span><span class="p">(</span><span class="n">err</span><span class="p">))</span>
					<span class="k">return</span>
				<span class="p">}</span>

				<span class="n">wg</span><span class="o">.</span><span class="n">Add</span><span class="p">(</span><span class="m">1</span><span class="p">)</span>
				<span class="k">go</span> <span class="n">wf</span><span class="o">.</span><span class="n">run</span><span class="p">(</span><span class="o">&amp;</span><span class="n">wg</span><span class="p">,</span> <span class="n">url</span><span class="p">,</span> <span class="n">id</span><span class="p">)</span>
			<span class="p">}</span>

			<span class="k">if</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">rows</span><span class="o">.</span><span class="n">Err</span><span class="p">();</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
				<span class="n">log</span><span class="o">.</span><span class="n">Error</span><span class="p">(</span><span class="s">"error during rows iteration"</span><span class="p">,</span> <span class="n">zap</span><span class="o">.</span><span class="n">Error</span><span class="p">(</span><span class="n">err</span><span class="p">))</span>
				<span class="k">return</span>
			<span class="p">}</span>

			<span class="n">wg</span><span class="o">.</span><span class="n">Wait</span><span class="p">()</span>
		<span class="p">}()</span>

		<span class="n">elapsed</span> <span class="o">:=</span> <span class="n">time</span><span class="o">.</span><span class="n">Since</span><span class="p">(</span><span class="n">start</span><span class="p">)</span>
		<span class="k">if</span> <span class="n">remaining</span> <span class="o">:=</span> <span class="m">5</span><span class="o">*</span><span class="n">time</span><span class="o">.</span><span class="n">Second</span> <span class="o">-</span> <span class="n">elapsed</span><span class="p">;</span> <span class="n">remaining</span> <span class="o">&gt;</span> <span class="m">0</span> <span class="p">{</span>
			<span class="n">time</span><span class="o">.</span><span class="n">Sleep</span><span class="p">(</span><span class="n">remaining</span><span class="p">)</span>
		<span class="p">}</span>
	<span class="p">}</span>
<span class="p">}</span>

<span class="k">func</span> <span class="p">(</span><span class="n">wf</span> <span class="o">*</span><span class="n">WorkerFunctions</span><span class="p">)</span> <span class="n">run</span><span class="p">(</span><span class="n">wg</span> <span class="o">*</span><span class="n">sync</span><span class="o">.</span><span class="n">WaitGroup</span><span class="p">,</span> <span class="n">url</span> <span class="kt">string</span><span class="p">,</span> <span class="n">id</span> <span class="kt">int64</span><span class="p">)</span> <span class="p">{</span>
	<span class="k">defer</span> <span class="n">wg</span><span class="o">.</span><span class="n">Done</span><span class="p">()</span>

	<span class="k">var</span> <span class="n">message</span> <span class="kt">string</span>

	<span class="k">if</span> <span class="n">id</span> <span class="o">&lt;</span> <span class="m">0</span> <span class="p">{</span>
		<span class="n">message</span> <span class="o">=</span> <span class="s">"invalid id: id must be non-negative"</span>
		<span class="n">log</span><span class="o">.</span><span class="n">Error</span><span class="p">(</span><span class="n">message</span><span class="p">)</span>
		<span class="n">setError</span><span class="p">(</span><span class="n">wf</span><span class="o">.</span><span class="n">db</span><span class="p">,</span> <span class="n">url</span><span class="p">,</span> <span class="n">message</span><span class="p">)</span>
		<span class="k">return</span>
	<span class="p">}</span>

	<span class="k">var</span> <span class="p">(</span>
		<span class="n">ctx</span>   <span class="o">=</span> <span class="n">context</span><span class="o">.</span><span class="n">Background</span><span class="p">()</span>
		<span class="n">short</span> <span class="o">=</span> <span class="n">base36</span><span class="o">.</span><span class="n">Encode</span><span class="p">(</span><span class="kt">uint64</span><span class="p">(</span><span class="n">id</span><span class="p">))</span>
	<span class="p">)</span>

	<span class="n">dir</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">os</span><span class="o">.</span><span class="n">MkdirTemp</span><span class="p">(</span><span class="s">""</span><span class="p">,</span> <span class="s">"screenshot"</span><span class="p">)</span>
	<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
		<span class="n">message</span> <span class="o">=</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Sprintf</span><span class="p">(</span><span class="s">"failed to create temporary directory: %v"</span><span class="p">,</span> <span class="n">err</span><span class="p">)</span>
		<span class="n">log</span><span class="o">.</span><span class="n">Error</span><span class="p">(</span><span class="n">message</span><span class="p">)</span>
		<span class="n">setError</span><span class="p">(</span><span class="n">wf</span><span class="o">.</span><span class="n">db</span><span class="p">,</span> <span class="n">url</span><span class="p">,</span> <span class="n">message</span><span class="p">)</span>
		<span class="k">return</span>
	<span class="p">}</span>
	<span class="k">defer</span> <span class="n">os</span><span class="o">.</span><span class="n">RemoveAll</span><span class="p">(</span><span class="n">dir</span><span class="p">)</span>

	<span class="k">var</span> <span class="p">(</span>
		<span class="n">fileName</span> <span class="o">=</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Sprintf</span><span class="p">(</span><span class="s">"%s.%s"</span><span class="p">,</span> <span class="n">short</span><span class="p">,</span> <span class="n">extension</span><span class="p">)</span>
		<span class="n">filePath</span> <span class="o">=</span> <span class="n">filepath</span><span class="o">.</span><span class="n">Join</span><span class="p">(</span><span class="n">dir</span><span class="p">,</span> <span class="n">fileName</span><span class="p">)</span>
	<span class="p">)</span>

	<span class="n">page</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">wf</span><span class="o">.</span><span class="n">browser</span><span class="o">.</span><span class="n">NewPage</span><span class="p">()</span>
	<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
		<span class="n">message</span> <span class="o">=</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Sprintf</span><span class="p">(</span><span class="s">"failed to create new page: %v"</span><span class="p">,</span> <span class="n">err</span><span class="p">)</span>
		<span class="n">log</span><span class="o">.</span><span class="n">Error</span><span class="p">(</span><span class="n">message</span><span class="p">)</span>
		<span class="n">setError</span><span class="p">(</span><span class="n">wf</span><span class="o">.</span><span class="n">db</span><span class="p">,</span> <span class="n">url</span><span class="p">,</span> <span class="n">message</span><span class="p">)</span>
		<span class="k">return</span>
	<span class="p">}</span>
	<span class="k">defer</span> <span class="n">page</span><span class="o">.</span><span class="n">Close</span><span class="p">()</span>

	<span class="k">if</span> <span class="n">_</span><span class="p">,</span> <span class="n">err</span> <span class="o">=</span> <span class="n">page</span><span class="o">.</span><span class="n">Goto</span><span class="p">(</span><span class="n">url</span><span class="p">,</span> <span class="n">playwright</span><span class="o">.</span><span class="n">PageGotoOptions</span><span class="p">{</span>
		<span class="n">WaitUntil</span><span class="o">:</span> <span class="n">playwright</span><span class="o">.</span><span class="n">WaitUntilStateDomcontentloaded</span><span class="p">,</span>
	<span class="p">});</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
		<span class="n">message</span> <span class="o">=</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Sprintf</span><span class="p">(</span><span class="s">"failed to navigate to url: %v"</span><span class="p">,</span> <span class="n">err</span><span class="p">)</span>
		<span class="n">log</span><span class="o">.</span><span class="n">Error</span><span class="p">(</span><span class="n">message</span><span class="p">)</span>
		<span class="n">setError</span><span class="p">(</span><span class="n">wf</span><span class="o">.</span><span class="n">db</span><span class="p">,</span> <span class="n">url</span><span class="p">,</span> <span class="n">message</span><span class="p">)</span>
		<span class="k">return</span>
	<span class="p">}</span>

	<span class="n">time</span><span class="o">.</span><span class="n">Sleep</span><span class="p">(</span><span class="n">time</span><span class="o">.</span><span class="n">Second</span> <span class="o">*</span> <span class="m">5</span><span class="p">)</span>

	<span class="n">title</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">page</span><span class="o">.</span><span class="n">Title</span><span class="p">()</span>
	<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
		<span class="n">message</span> <span class="o">=</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Sprintf</span><span class="p">(</span><span class="s">"could not get title: %v"</span><span class="p">,</span> <span class="n">err</span><span class="p">)</span>
		<span class="n">log</span><span class="o">.</span><span class="n">Error</span><span class="p">(</span><span class="n">message</span><span class="p">)</span>
		<span class="n">setError</span><span class="p">(</span><span class="n">wf</span><span class="o">.</span><span class="n">db</span><span class="p">,</span> <span class="n">url</span><span class="p">,</span> <span class="n">message</span><span class="p">)</span>
		<span class="k">return</span>
	<span class="p">}</span>

	<span class="n">description</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">page</span><span class="o">.</span><span class="n">Locator</span><span class="p">(</span><span class="s">`meta[name="description"]`</span><span class="p">)</span><span class="o">.</span><span class="n">GetAttribute</span><span class="p">(</span><span class="s">"content"</span><span class="p">)</span>
	<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
		<span class="n">log</span><span class="o">.</span><span class="n">Info</span><span class="p">(</span><span class="s">"could not get meta description"</span><span class="p">,</span> <span class="n">zap</span><span class="o">.</span><span class="n">Error</span><span class="p">(</span><span class="n">err</span><span class="p">))</span>
		<span class="n">description</span> <span class="o">=</span> <span class="s">""</span>
	<span class="p">}</span>

	<span class="k">if</span> <span class="n">_</span><span class="p">,</span> <span class="n">err</span> <span class="o">=</span> <span class="n">page</span><span class="o">.</span><span class="n">Screenshot</span><span class="p">(</span><span class="n">playwright</span><span class="o">.</span><span class="n">PageScreenshotOptions</span><span class="p">{</span>
		<span class="n">Path</span><span class="o">:</span> <span class="n">playwright</span><span class="o">.</span><span class="n">String</span><span class="p">(</span><span class="n">filePath</span><span class="p">),</span>
	<span class="p">});</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
		<span class="n">message</span> <span class="o">=</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Sprintf</span><span class="p">(</span><span class="s">"failed to create screenshot: %v"</span><span class="p">,</span> <span class="n">err</span><span class="p">)</span>
		<span class="n">log</span><span class="o">.</span><span class="n">Error</span><span class="p">(</span><span class="n">message</span><span class="p">)</span>
		<span class="n">setError</span><span class="p">(</span><span class="n">wf</span><span class="o">.</span><span class="n">db</span><span class="p">,</span> <span class="n">url</span><span class="p">,</span> <span class="n">message</span><span class="p">)</span>
		<span class="k">return</span>
	<span class="p">}</span>

	<span class="n">fp</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">os</span><span class="o">.</span><span class="n">Open</span><span class="p">(</span><span class="n">filePath</span><span class="p">)</span>
	<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
		<span class="n">message</span> <span class="o">=</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Sprintf</span><span class="p">(</span><span class="s">"failed to open screenshot: %v"</span><span class="p">,</span> <span class="n">err</span><span class="p">)</span>
		<span class="n">log</span><span class="o">.</span><span class="n">Error</span><span class="p">(</span><span class="n">message</span><span class="p">)</span>
		<span class="n">setError</span><span class="p">(</span><span class="n">wf</span><span class="o">.</span><span class="n">db</span><span class="p">,</span> <span class="n">url</span><span class="p">,</span> <span class="n">message</span><span class="p">)</span>
		<span class="k">return</span>
	<span class="p">}</span>
	<span class="k">defer</span> <span class="n">fp</span><span class="o">.</span><span class="n">Close</span><span class="p">()</span>

	<span class="n">image</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">google</span><span class="o">.</span><span class="n">NewImageFromReader</span><span class="p">(</span><span class="n">fp</span><span class="p">)</span>
	<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
		<span class="n">message</span> <span class="o">=</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Sprintf</span><span class="p">(</span><span class="s">"failed to load screenshot: %v"</span><span class="p">,</span> <span class="n">err</span><span class="p">)</span>
		<span class="n">log</span><span class="o">.</span><span class="n">Error</span><span class="p">(</span><span class="n">message</span><span class="p">)</span>
		<span class="n">setError</span><span class="p">(</span><span class="n">wf</span><span class="o">.</span><span class="n">db</span><span class="p">,</span> <span class="n">url</span><span class="p">,</span> <span class="n">message</span><span class="p">)</span>
		<span class="k">return</span>
	<span class="p">}</span>

	<span class="n">annotations</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">wf</span><span class="o">.</span><span class="n">vision</span><span class="o">.</span><span class="n">DetectSafeSearch</span><span class="p">(</span><span class="n">ctx</span><span class="p">,</span> <span class="n">image</span><span class="p">,</span> <span class="no">nil</span><span class="p">)</span>
	<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
		<span class="n">message</span> <span class="o">=</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Sprintf</span><span class="p">(</span><span class="s">"failed to detect labels: %v"</span><span class="p">,</span> <span class="n">err</span><span class="p">)</span>
		<span class="n">log</span><span class="o">.</span><span class="n">Error</span><span class="p">(</span><span class="n">message</span><span class="p">)</span>
		<span class="n">setError</span><span class="p">(</span><span class="n">wf</span><span class="o">.</span><span class="n">db</span><span class="p">,</span> <span class="n">url</span><span class="p">,</span> <span class="n">message</span><span class="p">)</span>
		<span class="k">return</span>
	<span class="p">}</span>

	<span class="k">if</span> <span class="n">annotations</span><span class="o">.</span><span class="n">Adult</span> <span class="o">&gt;=</span> <span class="n">visionpb</span><span class="o">.</span><span class="n">Likelihood_POSSIBLE</span> <span class="o">||</span> <span class="n">annotations</span><span class="o">.</span><span class="n">Violence</span> <span class="o">&gt;=</span> <span class="n">visionpb</span><span class="o">.</span><span class="n">Likelihood_POSSIBLE</span> <span class="o">||</span> <span class="n">annotations</span><span class="o">.</span><span class="n">Racy</span> <span class="o">&gt;=</span> <span class="n">visionpb</span><span class="o">.</span><span class="n">Likelihood_POSSIBLE</span> <span class="p">{</span>
		<span class="n">message</span> <span class="o">=</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Sprintf</span><span class="p">(</span><span class="s">"site is not safe %v"</span><span class="p">,</span> <span class="n">annotations</span><span class="p">)</span>
		<span class="n">log</span><span class="o">.</span><span class="n">Error</span><span class="p">(</span><span class="n">message</span><span class="p">)</span>
		<span class="n">setError</span><span class="p">(</span><span class="n">wf</span><span class="o">.</span><span class="n">db</span><span class="p">,</span> <span class="n">url</span><span class="p">,</span> <span class="n">message</span><span class="p">)</span>
		<span class="k">return</span>
	<span class="p">}</span>

	<span class="n">cmd</span> <span class="o">:=</span> <span class="n">exec</span><span class="o">.</span><span class="n">Command</span><span class="p">(</span><span class="s">"convert"</span><span class="p">,</span> <span class="n">filePath</span><span class="p">,</span> <span class="s">"-resize"</span><span class="p">,</span> <span class="s">"50%"</span><span class="p">,</span> <span class="s">"-filter"</span><span class="p">,</span> <span class="s">"Lanczos"</span><span class="p">,</span> <span class="s">"-quality"</span><span class="p">,</span> <span class="n">quality</span><span class="p">,</span> <span class="n">filePath</span><span class="p">)</span>
	<span class="k">if</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">cmd</span><span class="o">.</span><span class="n">Run</span><span class="p">();</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
		<span class="n">message</span> <span class="o">=</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Sprintf</span><span class="p">(</span><span class="s">"error during image conversion: %v"</span><span class="p">,</span> <span class="n">err</span><span class="p">)</span>
		<span class="n">log</span><span class="o">.</span><span class="n">Error</span><span class="p">(</span><span class="n">message</span><span class="p">)</span>
		<span class="n">setError</span><span class="p">(</span><span class="n">wf</span><span class="o">.</span><span class="n">db</span><span class="p">,</span> <span class="n">url</span><span class="p">,</span> <span class="n">message</span><span class="p">)</span>
		<span class="k">return</span>
	<span class="p">}</span>

	<span class="n">_</span><span class="p">,</span> <span class="n">err</span> <span class="o">=</span> <span class="n">wf</span><span class="o">.</span><span class="n">mc</span><span class="o">.</span><span class="n">FPutObject</span><span class="p">(</span><span class="n">ctx</span><span class="p">,</span> <span class="n">bucket</span><span class="p">,</span> <span class="n">fileName</span><span class="p">,</span> <span class="n">filePath</span><span class="p">,</span> <span class="n">minio</span><span class="o">.</span><span class="n">PutObjectOptions</span><span class="p">{</span><span class="n">ContentType</span><span class="o">:</span> <span class="n">mimetype</span><span class="p">})</span>
	<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
		<span class="n">message</span> <span class="o">=</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Sprintf</span><span class="p">(</span><span class="s">"failed to upload file to minio: %v"</span><span class="p">,</span> <span class="n">err</span><span class="p">)</span>
		<span class="n">log</span><span class="o">.</span><span class="n">Error</span><span class="p">(</span><span class="n">message</span><span class="p">)</span>
		<span class="n">setError</span><span class="p">(</span><span class="n">wf</span><span class="o">.</span><span class="n">db</span><span class="p">,</span> <span class="n">url</span><span class="p">,</span> <span class="n">message</span><span class="p">)</span>
		<span class="k">return</span>
	<span class="p">}</span>

	<span class="k">if</span> <span class="n">_</span><span class="p">,</span> <span class="n">err</span> <span class="o">=</span> <span class="n">wf</span><span class="o">.</span><span class="n">db</span><span class="o">.</span><span class="n">Exec</span><span class="p">(</span><span class="s">"UPDATE data SET ready = 1, title = ?, description = ? WHERE url = ?"</span><span class="p">,</span> <span class="n">title</span><span class="p">,</span> <span class="n">description</span><span class="p">,</span> <span class="n">url</span><span class="p">);</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
		<span class="n">message</span> <span class="o">=</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Sprintf</span><span class="p">(</span><span class="s">"failed to update database record: %v"</span><span class="p">,</span> <span class="n">err</span><span class="p">)</span>
		<span class="n">log</span><span class="o">.</span><span class="n">Error</span><span class="p">(</span><span class="n">message</span><span class="p">)</span>
		<span class="n">setError</span><span class="p">(</span><span class="n">wf</span><span class="o">.</span><span class="n">db</span><span class="p">,</span> <span class="n">url</span><span class="p">,</span> <span class="n">message</span><span class="p">)</span>
		<span class="k">return</span>
	<span class="p">}</span>

	<span class="k">go</span> <span class="n">warmup</span><span class="p">(</span><span class="n">fmt</span><span class="o">.</span><span class="n">Sprintf</span><span class="p">(</span><span class="s">"%s/%s"</span><span class="p">,</span> <span class="n">os</span><span class="o">.</span><span class="n">Getenv</span><span class="p">(</span><span class="s">"DOMAIN"</span><span class="p">),</span> <span class="n">short</span><span class="p">))</span>
<span class="p">}</span>

<span class="k">func</span> <span class="n">setError</span><span class="p">(</span><span class="n">db</span> <span class="o">*</span><span class="n">sql</span><span class="o">.</span><span class="n">DB</span><span class="p">,</span> <span class="n">url</span><span class="p">,</span> <span class="n">message</span> <span class="kt">string</span><span class="p">)</span> <span class="p">{</span>
	<span class="k">if</span> <span class="n">_</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">db</span><span class="o">.</span><span class="n">Exec</span><span class="p">(</span><span class="s">"UPDATE data SET ready = 1, error = ? WHERE url = ?"</span><span class="p">,</span> <span class="n">message</span><span class="p">,</span> <span class="n">url</span><span class="p">);</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
		<span class="n">log</span><span class="o">.</span><span class="n">Error</span><span class="p">(</span><span class="s">"failed to update database error record"</span><span class="p">,</span> <span class="n">zap</span><span class="o">.</span><span class="n">Error</span><span class="p">(</span><span class="n">err</span><span class="p">))</span>
	<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>It’s noticeable that it took a bit more effort than if I had used a task queue. However, it turned out to be quite robust and easy to debug, and for that reason alone, it was worth it.</p>

<h3 id="conclusion">Conclusion</h3>

<p>In sharing the shortened link (<a href="https://takealook.pro/4MP">https://takealook.pro/4MP</a>) for my company <a href="https://ultratech.software/">Ultratech Software</a> on social networks, you can have a really nice preview, as seen below.</p>

<p><img src="/public/2024-06-05-how-i-saved-a-few-dozen-dollars-by-migrating-my-personal-projects-from-the-cloud-to-a-raspberry-pi/takealook.pro.avif" alt="takealook.pro" class="center" /></p>

<p><a href="https://takealook.pro/">Take A Look</a></p>]]></content><author><name>Rodrigo Delduca</name><email>rodrigo@delduca.org</email></author><summary type="html"><![CDATA[Intro]]></summary></entry></feed>