<?xml version="1.0" encoding="utf-8"?>
  <feed xmlns="http://www.w3.org/2005/Atom">
      <id>https://pfy.ch</id>
      <title>Pfy.ch</title>
      <author>
        <name>Pfych</name>
      </author>
      <updated>2026-04-27T04:56:51.290Z</updated>
      <link href="https://pfy.ch"/>
      <entry>
                  <id>https://pfy.ch/weekly/2026/17.html</id>
                  <title>Paris &amp; London</title>
                  <published>2026-04-27T04:32:18.859Z</published>
                  
                  <link>https://pfy.ch/weekly/2026/17.html</link>
                  <emoji>📅</emoji>
                  <content type="html" xml:base="https://pfy.ch/weekly/2026/17.html"><![CDATA[ <p>I&#39;m currently flying home after my 2-week Europe trip meeting heaps of awesome people. Like I mentioned in last weeks post, ideally I plan to write a dedicated &quot;Europe Trip&quot; post alongside these weekly updates, but last week we did 4 nights in Paris and 4 nights in London!</p>
<p>I didn&#39;t know what to expect out of Paris, and I kinda thought I&#39;d not fuck with London - but in reality, both my partner and I really fucked with London, and kinda really didn&#39;t enjoy Paris (Other than hanging out with a friend!<sup><a id="footnote-ref-1" href="#footnote-1" data-footnote-ref aria-describedby="footnote-label">1</a></sup>). </p>
<p>Paris was kinda rough initially, insanely busy and unwelcoming. It wasn&#39;t until we went a bit out of the city that we actually could enjoy how pretty everything was, and I do mean that. Paris was Beautiful - but it was just to packed and had way too many cars driving around. We met up with a local friend a few times in the city, and it was great spending time with her and yapping. Kinda stopped my partner and I from going crazy - and she helped us enjoy the city a lot more 💖.</p>
<p>We didn&#39;t do too many touristy things, but we did see a few things - you really can&#39;t appreciate the scale of all these monuments &amp; buildings until you&#39;re up close and in person with them! Our friend recommended we visit when the locals are on holiday, since we ended up visiting when theres tourists <em>and</em> locals - which made things super packed...</p>
<p>After our short Paris trip we went through to London, we caught the Eurostar in and the whole process felt kinda tacked on and hacky lol. I had to scan my passport at 3 different terminals in the same room to board the train, all doing different things haha. Brexit moment I guess.</p>
<p>London was great because we got to spend most of the time meeting and hanging out with friends in pubs<sup><a id="footnote-ref-2" href="#footnote-2" data-footnote-ref aria-describedby="footnote-label">2</a></sup> - with a few days just spent chilling in parks! <strong><em>Sadly I am allergic to the UK...</em></strong> I&#39;ve never really had allergies before but I basically was on a rotation of different meds to stop me feeling like death in and about the city, not sure if it was damp or pollen. But something was fucking me up bad !!</p>
<p>I&#39;ve gone through my camera and I&#39;ve gotten some great pictures so I&#39;m keen to post them all a bit later in the week when I get time for the full blown holiday post!</p>
<section class="footnotes" data-footnotes>
<h2 id="footnote-label" class="sr-only">Footnotes</h2>
<ol>
<li id="footnote-1">
<p>ty, without u we would have crashed out from stress - we loved hanging out !! <a href="#footnote-ref-1" data-footnote-backref aria-label="Back to reference 1">↩</a></p>
</li>
<li id="footnote-2">
<p>It literally just felt like home but with more transphobia<sup><a id="footnote-ref-3" href="#footnote-3" data-footnote-ref aria-describedby="footnote-label">3</a></sup> (and funny accents). <a href="#footnote-ref-2" data-footnote-backref aria-label="Back to reference 2">↩</a></p>
</li>
<li id="footnote-3">
<p>Genuinely though - its actually fucked. We were there for 4 days and had 1 direct incident and another few ones by proxy with friends we were meeting with??? UK Moment? <a href="#footnote-ref-3" data-footnote-backref aria-label="Back to reference 3">↩</a></p>
</li>
</ol>
</section>
 ]]></content>
                </entry><entry>
                  <id>https://pfy.ch/weekly/2026/16.html</id>
                  <title>Amsterdam</title>
                  <published>2026-04-21T13:27:10.822Z</published>
                  
                  <link>https://pfy.ch/weekly/2026/16.html</link>
                  <emoji>📅</emoji>
                  <content type="html" xml:base="https://pfy.ch/weekly/2026/16.html"><![CDATA[ <p>Turns out it&#39;s really difficult to stick to a blogging cadence when you&#39;re travelling. My partner and I are actually in Paris right now, but we spent the last week with friends in Amsterdam! </p>
<p>We spent the week in a BnB in Heiloo - an actual BnB. The hosts also lived in the house still, but the floorplan had been changed so we had a private kitchen/bathroom/bedrooms. They cooked us some yummy treats during our week there, and they were very friendly!</p>
<p>The day we landed we met with everyone at Amsterdam Central - I got barely any sleep on the flight (RIP), but when we met with our friends they tried to keep us busy around the city. We got coffee and bagels, and just wandered around - it was very pretty, but we definetly started to crash early. It was about an hour from central to Heiloo, both my partner and I spilt off to go straight to the BnB while everyone else went to go get groceries for dinner &amp; breakfast. Two of our friends ended up cooking bolognaise for all eight of us, and it was really yummy. But we crashed soon after...</p>
<p>The rest of the days there were all very low-key. Nice coffee at Dak Showroom, Stedelijk Museum, Wandering in Utrecht - <em>lots</em> of wandering, Meeting with old friends at The Hague, it was all very chill. Nothing crazy, nothing super touristy, just spending time with friends. It&#39;s honestly been the best holiday I&#39;ve had so far.</p>
<p>Heiloo was absolutely beautiful, the whole country was. Amsterdam definitely felt super touristy, but it was easy enough to escape. We 100% want to go back, and ideally meet with everyone again.</p>
<p>I&#39;m planning to do one more blog post with a short update on our time in Paris and London - followed by a bigger post with all the photos I took and some more details!</p>
 ]]></content>
                </entry><entry>
                  <id>https://pfy.ch/weekly/2026/15.html</id>
                  <title>Off to Europe!</title>
                  <published>2026-04-14T08:11:14.852Z</published>
                  
                  <link>https://pfy.ch/weekly/2026/15.html</link>
                  <emoji>📅</emoji>
                  <content type="html" xml:base="https://pfy.ch/weekly/2026/15.html"><![CDATA[ <p>Oops! We&#39;re a day late ... But I have a really good excuse this time - I&#39;m in Amsterdam! </p>
<p>Most of the week was spent just doing not much and prepping for the Holiday, We flew out of Sydney on the Sunday at around midday and transferred in Jakarta for another flight directly to Amsterdam. All up it was about 23 hours or something on a plane and it <em>sucked!</em> We landed in AMS at about 9am, so I was really hoping I would pass out on the last leg of the flight and be well rested for the day but that didn&#39;t really end up happening lol - I got maybe an hour max of sleep ...</p>
<p>My partner and I did watch a bunch of movies on the plane though:</p>
<ul>
<li>Predator Badlands</li>
<li>Godzilla: King of the Monsters </li>
<li>The Prestige</li>
<li>Zootopia 2</li>
<li>Detective Pikachu</li>
<li>The Lego Batman movie</li>
</ul>
<p>Detective Pikachu was actually terrible, and we didn&#39;t end up finishing it - it felt like an old Smosh YouTube bit with too much budget. Otherwise, I enjoyed all the movies heaps! Predator was really good, Godzilla was mid but campy (Holy fuck they had to do so much rewriting in future movies lul), The Prestige was super intresting, Zooptopia 2 was really fun, and the Lego Batman Movie was funny. They at least helped pass the time! Will probably rewatch when I get back home on a nicer screen and speakers.</p>
<p>Once we landed in AMS my partner and I met up straight away with friends - ideally they would keep us awake until we got back to our accommodation that night so we wouldn&#39;t have shitty jet lag. It worked!</p>
<p>We did a lot of wandering around - If we sat on a particularly comfortable bench I probably would have fallen asleep, but I didn&#39;t! </p>
<p>I plan to write all about the trip in next week&#39;s post!</p>
 ]]></content>
                </entry><entry>
                  <id>https://pfy.ch/weekly/2026/14.html</id>
                  <title>Easter Long Weekend</title>
                  <published>2026-04-06T05:35:18.434Z</published>
                  
                  <link>https://pfy.ch/weekly/2026/14.html</link>
                  <emoji>📅</emoji>
                  <content type="html" xml:base="https://pfy.ch/weekly/2026/14.html"><![CDATA[ <p>This was another slow week - It actually started with going to another idol show to support my friend! It was a chill show at a bar called Rouge in Circular Quay. The vibe was chill, and it was fun supporting friends again! The bar had booths so we grabbed one and watched the show while enjoying our drinks. Good way to start the week!</p>
<p>Other than that though it&#39;s been a really low-key week, just working and getting things done. Still tinkering with Nix &amp; NixOS in my free time. Really enjoying it! I&#39;ve also been more online lately and talking with oomfies on Signal &amp; Bsky - which has been really fun.</p>
<p>For the long weekend, my partner and I really didn&#39;t get up too much. Mostly chilled at home, played games and read manga. I binged and caught up on an entire series yesterday<sup><a id="footnote-ref-1" href="#footnote-1" data-footnote-ref aria-describedby="footnote-label">1</a></sup> - was really good! Planning to wrap up the Easter Monday with a TV binge, not sure of what yet, but we&#39;ve got snacks!</p>
<p>Next week is a short one, only Tuesday - Friday. I&#39;ve got a salon appointment on Wednesday &amp; my partner and I fly to Amsterdam on Sunday! Next week&#39;s blog post might be a little delayed since we land Monday morning in Amsterdam!</p>
<section class="footnotes" data-footnotes>
<h2 id="footnote-label" class="sr-only">Footnotes</h2>
<ol>
<li id="footnote-1">
<p>It&#39;s called ぜんぶ壊して地獄で愛して <sup>Toxic romance!! - Content warning!</sup> <a href="#footnote-ref-1" data-footnote-backref aria-label="Back to reference 1">↩</a></p>
</li>
</ol>
</section>
 ]]></content>
                </entry><entry>
                  <id>https://pfy.ch/weekly/2026/13.html</id>
                  <title>The social week</title>
                  <published>2026-03-30T04:59:56.844Z</published>
                  
                  <link>https://pfy.ch/weekly/2026/13.html</link>
                  <emoji>📅</emoji>
                  <content type="html" xml:base="https://pfy.ch/weekly/2026/13.html"><![CDATA[ <p>A complete flip on last week! Last week felt kinda shitty because I was a little bit of a shut-in - although the post for last week focused mostly on the positive, I was masking a bit lol. We started this week by going to the Arcade after work and just playing games, it&#39;s actually been a while since I&#39;ve played so it was really fun getting back into things. I&#39;ve got blisters on my hands still from playing Gitadora Drums for a few hours, and the day after my legs were super sore from the 1 set of DDR I played with my partner<sup><a id="footnote-ref-1" href="#footnote-1" data-footnote-ref aria-describedby="footnote-label">1</a></sup>. I&#39;m so washed, but it was still fun playing fun charts - even if I didn&#39;t set any new PBs.</p>
<p>On Wednesday I had a chill day in office and did a last minute dinner with some homies at a Korean place. Didn&#39;t do BBQ, just ordered a bunch of yummy things and beers and chatted it up for an hour. It was fun just doing something out of the blue with friends + getting out of the house. I similarly had an impromptu lunch with my brother, uncle &amp; grandpa on the Friday. I literally cannot remember the last time I had a good chat with my uncle or grandpa so it was really nice having a few drinks and eating gyoza &amp; karaage!</p>
<p>I also did another shopping trip! I bought 3 new skirts from Uniqlo during my lunch break on Thursday &amp; spent that afternoon working from a coworking space in the city. It was good to shake up the scenery and backdrop when working. I 100% need to do it more, sitting at the same desk every day at home drives you a little crazy! I also spent that night finally digging into Nix! I ended up writing <a class=""  href="../../programming/nix/good-actually.html">a blog post just for that</a> since I actually have finally gotten hooked on it - I set up my new laptop almost entirely with Nix! Which was actually really convenient since over the weekend the screen started having random lines show up in it ;-;;; !!! Luckily it&#39;s covered under warranty, but I&#39;ll be out of a laptop for a week, and they might wipe it - but Nix fixes this<sup><a id="footnote-ref-2" href="#footnote-2" data-footnote-ref aria-describedby="footnote-label">2</a></sup>.</p>
<p>It was a pretty jam packed week, but I also planned catching up with my parents for lunch over the weekend. After last week I was like &quot;I need to get outside&quot; and I organised a lunch with them. It was really nice - but it made the whole week really busy since I didn&#39;t expect to do everything else as well haha.</p>
<p>That&#39;s everything for that week though, this coming week should hopefully be more chill :3c !</p>
<section class="footnotes" data-footnotes>
<h2 id="footnote-label" class="sr-only">Footnotes</h2>
<ol>
<li id="footnote-1">
<p>Astrogazer, Pluto, 強風オールバック, CUE CUE RESCUE <a href="#footnote-ref-1" data-footnote-backref aria-label="Back to reference 1">↩</a></p>
</li>
<li id="footnote-2">
<p>3 Days from learning Nix → &quot;Nix fixes this&quot; <a href="#footnote-ref-2" data-footnote-backref aria-label="Back to reference 2">↩</a></p>
</li>
</ol>
</section>
 ]]></content>
                </entry><entry>
                  <id>https://pfy.ch/programming/nix/good-actually.html</id>
                  <title>Nix is good actually?</title>
                  <published>2026-03-26T10:32:49.299Z</published>
                  
                  <link>https://pfy.ch/programming/nix/good-actually.html</link>
                  <emoji>💻</emoji>
                  <content type="html" xml:base="https://pfy.ch/programming/nix/good-actually.html"><![CDATA[ <p>Nix has been one of those things that&#39;s sat on my &quot;that seems really cool&quot; list for almost a year now. I&#39;ve actually tried it a few times, but always bounce off at the docs or at the thought of all the &quot;work&quot; I&#39;m going to have to do.</p>
<p>Well, today, I was unmedicated &amp; while trying to work on something else, my brain decided: &quot;Hey I reckon we could try and do nix, but like, just dotfiles&quot;. </p>
<p>I don&#39;t know why I never thought of this, I always see big grand setups - or huge NixOS repos, but it makes sense when thinking about things like <code>home-manager</code>. So I just sorta tried to do it.</p>
<p>I do have to admit - the docs are <strong><em>still fucked.</em></strong> But with the help of an LLM, Some oomfs configs<sup><a id="footnote-ref-1" href="#footnote-1" data-footnote-ref aria-describedby="footnote-label">1</a></sup>, and a general idea of how things should work I ended up with a pretty simple &amp; sane setup that I actually like and understand.</p>
<p>I <em>really</em> do not fuck with the idea of having Claude write my entire config for me, so I really mostly used it to point me in the right direction. Ever since I did up my own nvim config myself, I&#39;ve very much been in the boat of &quot;you should write your own, and understand your own dotfiles&quot;. When something inevitably breaks, you&#39;re reading someone else&#39;s code if you didn&#39;t write it. That adds even more overhead and mental energy when you&#39;re trying to debug.</p>
<p>I started super small, just getting git &amp; my gpg key setup. Since I do that a lot and it&#39;s always kind of annoying. I started with a new repo and a basic flake, I want two machines - Work &amp; Personal:</p>
<pre><code class="hljs language-nix">{
  <span class="hljs-attr">description</span> <span class="hljs-operator">=</span> <span class="hljs-string">&quot;pfych dotfiles&quot;</span>;

  <span class="hljs-attr">inputs</span> <span class="hljs-operator">=</span> {
    <span class="hljs-attr">nixpkgs.url</span> <span class="hljs-operator">=</span> <span class="hljs-string">&quot;https://channels.nixos.org/nixpkgs-unstable/nixexprs.tar.xz&quot;</span>;

    <span class="hljs-attr">home-manager</span> <span class="hljs-operator">=</span> {
      <span class="hljs-attr">type</span> <span class="hljs-operator">=</span> <span class="hljs-string">&quot;github&quot;</span>;
      <span class="hljs-attr">owner</span> <span class="hljs-operator">=</span> <span class="hljs-string">&quot;nix-community&quot;</span>;
      <span class="hljs-attr">repo</span> <span class="hljs-operator">=</span> <span class="hljs-string">&quot;home-manager&quot;</span>;
      <span class="hljs-attr">inputs.nixpkgs.follows</span> <span class="hljs-operator">=</span> <span class="hljs-string">&quot;nixpkgs&quot;</span>;
    };
  };

  <span class="hljs-attr">outputs</span> <span class="hljs-operator">=</span>
    { nixpkgs, home-manager, ... }:
    {
      <span class="hljs-attr">homeConfigurations</span> <span class="hljs-operator">=</span> {
        <span class="hljs-string">&quot;pfych@personal&quot;</span> <span class="hljs-operator">=</span> home-manager.lib.homeManagerConfiguration {
          <span class="hljs-attr">pkgs</span> <span class="hljs-operator">=</span> nixpkgs.legacyPackages.aarch64-darwin;
          <span class="hljs-attr">modules</span> <span class="hljs-operator">=</span> [
            <span class="hljs-symbol">./home/shared</span>
            <span class="hljs-symbol">./home/personal.nix</span>
          ];
        };

        <span class="hljs-string">&quot;pfych@work&quot;</span> <span class="hljs-operator">=</span> home-manager.lib.homeManagerConfiguration {
          <span class="hljs-attr">pkgs</span> <span class="hljs-operator">=</span> nixpkgs.legacyPackages.aarch64-darwin;
          <span class="hljs-attr">modules</span> <span class="hljs-operator">=</span> [
            <span class="hljs-symbol">./home/shared</span>
            <span class="hljs-symbol">./home/work.nix</span>
          ];
        };
      };
    };
}
</code></pre><p>It looks like a lot, but it&#39;s really not - Nix has some funky syntax, but it&#39;s all functions all the way down. They look like this:</p>
<pre><code class="hljs language-nix">{ <span class="hljs-attr">value</span> <span class="hljs-operator">=</span> { input, ... }: { <span class="hljs-attr">foo</span> <span class="hljs-operator">=</span> <span class="hljs-string">&quot;bar&quot;</span>; }; }
</code></pre><p>In a fucked up comparison, think of it like this js:</p>
<pre><code class="hljs language-typescript"><span class="hljs-keyword">const</span> value = (<span class="hljs-function">(<span class="hljs-params">input, ...rest</span>) =&gt;</span> ({<span class="hljs-attr">foo</span>: <span class="hljs-string">&quot;bar&quot;</span>}))()
</code></pre><p>So this base file defines a few variables: <code>description</code>, <code>inputs</code>, and <code>outputs</code>.</p>
<p><code>inputs</code> is where all our packages come from, I&#39;ve just got standard nixpkgs and home manager. These get passed into <code>outputs</code>! In my <code>outputs</code> I then configure everything via <code>home-manager</code> with two separate setups for both personal and work use.</p>
<p>What&#39;s cool about this is I can switch between them really easily:</p>
<pre><code class="hljs language-bash">home-manager switch --flake <span class="hljs-string">&quot;.#pfych@personal&quot;</span>
home-manager switch --flake <span class="hljs-string">&quot;.#pfych@work&quot;</span>
</code></pre><p>and everything flips over like nothing happened!</p>
<p>The actual home configs themselves are quite simple too, I point nix to the right packages and then import modules - these are just separate files super similar to this one! Let&#39;s look at <code>personal.nix</code>:</p>
<pre><code class="hljs language-nix">{ pkgs, ... }:
{
  <span class="hljs-attr">home.username</span> <span class="hljs-operator">=</span> <span class="hljs-string">&quot;pfych&quot;</span>;
  <span class="hljs-attr">home.homeDirectory</span> <span class="hljs-operator">=</span> <span class="hljs-string">&quot;/Users/pfych&quot;</span>;

  <span class="hljs-attr">services.gpg-agent.pinentry.package</span> <span class="hljs-operator">=</span> pkgs.pinentry_mac;
}
</code></pre><p>It&#39;s a function, that takes in packages (that we don&#39;t actually use 💀), and then it sets some variables that are system dependant. Like usernames, home directories, etc. Ideally I&#39;d just have one <code>darwin.nix</code> and <code>linux.nix</code> but my Work setup is slightly different so it won&#39;t be that simple ;-;;</p>
<p>The more interesting file is <code>./shared</code>! This is where the main config is! Shared is actually a folder containing a few files: <code>git.nix</code>, <code>gpg.nix</code> and finally <code>default.nix</code> - when you import a folder as a module nix looks for <code>default.nix</code> to import.</p>
<p>Here&#39;s what these files look like:</p>
<pre><code class="hljs language-nix">{ pkgs, ... }:
{
  <span class="hljs-attr">home.stateVersion</span> <span class="hljs-operator">=</span> <span class="hljs-string">&quot;24.11&quot;</span>;
  <span class="hljs-attr">programs.home-manager.enable</span> <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>;

  <span class="hljs-attr">imports</span> <span class="hljs-operator">=</span> [
    <span class="hljs-symbol">./git.nix</span>
    <span class="hljs-symbol">./gpg.nix</span>
  ];

  <span class="hljs-attr">home.packages</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">with</span> pkgs; [
    gnupg
  ];
}
</code></pre><p>The <code>default.nix</code> first enables home manager for everyone and sets a stateVersion. I then import my git &amp; gpg config, and then install <code>gnupg</code> with <code>home.packages</code>. This part is neat since it actually installs <code>gnupg</code> in my <code>$PATH</code> regardless of OS - so I don&#39;t need to use <code>homebrew</code> or <code>pacman</code>, etc.</p>
<p>My <code>git.nix</code> and <code>gpg.nix</code> then just have some basic config I&#39;d like to share everywhere:</p>
<pre><code class="hljs language-nix">{ ... }:
{
  <span class="hljs-attr">programs.git</span> <span class="hljs-operator">=</span> {
    <span class="hljs-attr">enable</span> <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>;

    <span class="hljs-attr">settings</span> <span class="hljs-operator">=</span> {
      <span class="hljs-attr">user</span> <span class="hljs-operator">=</span> {
        <span class="hljs-attr">name</span> <span class="hljs-operator">=</span> <span class="hljs-string">&quot;pfych&quot;</span>;
        <span class="hljs-attr">email</span> <span class="hljs-operator">=</span> <span class="hljs-string">&quot;contact@pfy.ch&quot;</span>;
      };

      <span class="hljs-attr">init.defaultBranch</span> <span class="hljs-operator">=</span> <span class="hljs-string">&quot;main&quot;</span>;
      <span class="hljs-attr">commit.gpgSign</span> <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>;
      <span class="hljs-attr">tag.gpgSign</span> <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>;
      <span class="hljs-attr">push.autoSetupRemote</span> <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>;
    };

    <span class="hljs-attr">signing</span> <span class="hljs-operator">=</span> {
      <span class="hljs-attr">key</span> <span class="hljs-operator">=</span> <span class="hljs-string">&quot;347543D390119D28232AA5D64CC16AAFFB23D192&quot;</span>;
      <span class="hljs-attr">format</span> <span class="hljs-operator">=</span> <span class="hljs-string">&quot;openpgp&quot;</span>;
    };
  };
}
</code></pre><p>and</p>
<pre><code class="hljs language-nix">{ ... }:
{
  <span class="hljs-attr">services.gpg-agent</span> <span class="hljs-operator">=</span> {
    <span class="hljs-attr">enable</span> <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>;
    <span class="hljs-attr">enableSshSupport</span> <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>;
    <span class="hljs-attr">sshKeys</span> <span class="hljs-operator">=</span> [
      <span class="hljs-string">&quot;D2785A4719E88BFEBB1F713B1E83E7CF9AB92225&quot;</span>
    ];
  };
}
</code></pre><p>This is really cool! So far it basically is just mirroring my existing <code>yadm</code> dotfile repo but using nix gives me a nicer lsp, typechecking and other goodies when working with all different applications configs!</p>
<p>The last little bit with nix that always confused me was &quot;Where the fuck does all this config go?&quot; - &quot;Everyone shares cool repos, but when I set up xyz everything was in <code>/etc</code>??&quot;. Well, it turns out I&#39;m kinda dumb - the answer is <em>it doesn&#39;t really matter!</em></p>
<p>I&#39;ve just got the repo set up in my home folder as <code>~/nix</code>, but I could just as easily move it to <code>~/.config/nix</code> or <code>~/Developer/nix</code> if I wanted to - It&#39;d just change the commands to build a little:</p>
<pre><code class="hljs language-bash"><span class="hljs-comment"># Assuming I&#x27;m at ~ and my config is in ~/Developer/nix</span>
home-manager switch --flake <span class="hljs-string">&quot;./Developer/nix#pfych@personal&quot;</span>
</code></pre><p>I&#39;m glad I pushed a little and actually tried to get something working this time. I&#39;m going to slowly start moving my existing <a class="externalLink" target="_blank" href="https://git.pfy.ch/pfych/unix-config">dotfiles repo</a> over to my <a class="externalLink" target="_blank" href="https://git.pfy.ch/pfych/nix">nix repo</a> whenever I have some downtime. Just like my dotfiles repo, I&#39;m content once it&#39;s set up and I don&#39;t find myself fiddling with them, &amp; I&#39;m hoping the same thing extends to nix!</p>
<p>So far I&#39;ve managed to move over my nvim config &amp; my Firefox config! Firefox was the coolest since I have all my chrome, plugins and settings sync&#39;d now with nix instead of doing weird symlink stuff with <code>yadm</code>!</p>
<p><bsky-comments data-post-id="3mhxg7qpfkk2h"></bsky-comments></p>
<section class="footnotes" data-footnotes>
<h2 id="footnote-label" class="sr-only">Footnotes</h2>
<ol>
<li id="footnote-1">
<p>💖 <a class="externalLink" target="_blank" href="https://isabelroses.com/">Isabel</a> &amp; <a class="externalLink" target="_blank" href="https://olaren.dev/en/">Olaren</a> <a href="#footnote-ref-1" data-footnote-backref aria-label="Back to reference 1">↩</a></p>
</li>
</ol>
</section>
 ]]></content>
                </entry><entry>
                  <id>https://pfy.ch/weekly/2026/12.html</id>
                  <title>Crash-out?</title>
                  <published>2026-03-23T04:06:57.758Z</published>
                  
                  <link>https://pfy.ch/weekly/2026/12.html</link>
                  <emoji>📅</emoji>
                  <content type="html" xml:base="https://pfy.ch/weekly/2026/12.html"><![CDATA[ <p>Honestly, this week and even part of last week I&#39;ve been slowly having a bit of a crash. I&#39;ve sort of been noticing it in the back of my mind but recently people around me have been noticing it as well. Apparently I&#39;m pretty consistent when it comes to my crash-outs, and apparently this one is delayed, but still technically on schedule lol. Saying &quot;Crash Out&quot; makes it seem like it&#39;s destructive and terrible and &quot;Oh no!!!&quot; but I really don&#39;t know another way to describe it - maybe a depressive slump? That&#39;s too sad lol.</p>
<p>Whenever this happens I do try to be positive though, reflect on what I&#39;ve been up to, and things I&#39;ve done. It really helps when my partner has a really good memory<sup><a id="footnote-ref-1" href="#footnote-1" data-footnote-ref aria-describedby="footnote-label">1</a></sup>, since she can always remind me of what we&#39;ve been up to - &amp; I&#39;m really appreciative of it. Last weekend was a bit slow, one of those &quot;chill &amp; quiet&quot; weekends - so it feels like you did nothing and wasted time, but we did actually go out and have a nice lunch and get a run in. It&#39;s small things that I want to try to focus on more.</p>
<p>Another thing I find that helps me remember things is <em>taking more photos!</em> It&#39;s soooo good to look back in my camera roll and see pictures from the past week to help me reflect on what I&#39;ve been up to. We actually started last week with a quick IKEA run - IKEA is actually super chill if you go at 6pm on a Monday vs the weekend lol. We grabbed a mirror (for selfies), some fake plants and a new desk for the study. The fake plants are mostly for hard to reach background greenery, things that u don&#39;t rlly pay much attention to. We prefer real plants where we can but some spots, like on top of bookshelves, aren&#39;t really realistic to easily water, and they&#39;re back there enough that when combo&#39;d with real plants it&#39;ll look nicer. We also grabbed a desk for our study, which until now was a glorified storage room, but we&#39;ve cleaned it out and have moved the 3D printers in there for now. Excited to use the space a bit more in the future!</p>
<p>On Tuesday we went over to a friend&#39;s place for dinner, as well as to pick up my new Macbook Air. We just chilled out and played with their cats &amp; they got me some gifts for my birthday too which was really nice. One friend got me the book &quot;Strange Houses&quot; which I was saying I really wanted to read, so I&#39;m keen to start reading it soon!</p>
<p>The rest of the week was quite slow and average - nothing too cool to focus on. I think that&#39;s why over the weekend I was kind of crashing, feeling like I did nothing, when really I <em>did</em> do shit - it was just at the start of the week. Maybe I need to do these reflective posts more often.</p>
<section class="footnotes" data-footnotes>
<h2 id="footnote-label" class="sr-only">Footnotes</h2>
<ol>
<li id="footnote-1">
<p>I swear it&#39;s photographic! <a href="#footnote-ref-1" data-footnote-backref aria-label="Back to reference 1">↩</a></p>
</li>
</ol>
</section>
 ]]></content>
                </entry><entry>
                  <id>https://pfy.ch/weekly/2026/11.html</id>
                  <title>Birthday!!</title>
                  <published>2026-03-16T03:31:54.873Z</published>
                  
                  <link>https://pfy.ch/weekly/2026/11.html</link>
                  <emoji>📅</emoji>
                  <content type="html" xml:base="https://pfy.ch/weekly/2026/11.html"><![CDATA[ <p>Last week was both a chill week and a big week! </p>
<p>It was my 25th Birthday on Saturday which I enjoyed with my partner by going out for a nice brunch, window shopping and relaxing. She also booked in a surprise birthday dinner with some friends on the Friday beforehand, we went to Yebisu and ate lots of yummy food (and drank a bit) - it was good fun. Everyone chipped in and got me lots of <a class="externalLink" target="_blank" href="https://bsky.app/profile/pfy.ch/post/3mgy25blkcs2b">trinkets and goodies</a> from TGSWIIWAGAA<sup><a id="footnote-ref-1" href="#footnote-1" data-footnote-ref aria-describedby="footnote-label">1</a></sup> as well as a CD of &quot;Lonely people with power&quot; by Deafheaven, which was really nice :3c !! I also got sent some postcards and cards from Family which are up on the fridge, even the ones from my siblings calling me old.,,</p>
<p>I managed to get the laptop I mentioned <a class=""  href="10.html">last week</a>! I&#39;ve got an M4 MacBook Air with 16GB of RAM for AUD$800, Absolute steal! I pick it up this week on Tuesday so it&#39;s exciting! Much better than the Neo I had my eyes on.</p>
<p>Otherwise, the week was kind of slow. I played a bit more IIDX &amp; Gitadora Drums on the weekend while out with friends and I really enjoyed it. I need to get back into the habbit of going into the city and playing more, I&#39;m a bit washed so it&#39;d be good to get back into it. I did also manage to build out a &quot;real time&quot; <a class=""  href="/#estrogen-levels">estrogen level monitor</a> for my homepage. It&#39;s very simple right now and just works on a basic generalized Bateman function, but I think it&#39;s funny. I&#39;ll probably do a writeup on it when it&#39;s got a bit more polish.</p>
<section class="footnotes" data-footnotes>
<h2 id="footnote-label" class="sr-only">Footnotes</h2>
<ol>
<li id="footnote-1">
<p>A manga series called &quot;The guy she was interested in wasn&#39;t a guy at all&quot;. It&#39;s really good!! <a href="#footnote-ref-1" data-footnote-backref aria-label="Back to reference 1">↩</a></p>
</li>
</ol>
</section>
 ]]></content>
                </entry><entry>
                  <id>https://pfy.ch/weekly/2026/10.html</id>
                  <title>New(?)* laptop!</title>
                  <published>2026-03-09T09:35:00.299Z</published>
                  
                  <link>https://pfy.ch/weekly/2026/10.html</link>
                  <emoji>📅</emoji>
                  <content type="html" xml:base="https://pfy.ch/weekly/2026/10.html"><![CDATA[ <p>Another week of just sorta chilling out - Work has been the usual, nothing crazy there. Managed to get a run in which was nice, even if it was 95% humidity,,,,</p>
<p>I&#39;ve been wanting to get myself a personal laptop for a while now. I&#39;ve been using an old ThinkPad T440, but it&#39;s mostly a tinkering device that I use to fuck around with things like nix and other Linux whatevers. I actually got thinking about it again with the MacBook Neo announcement. The fact the first thing I thought about was the RAM probably says it&#39;s not for me, but it did get me talking out loud again to my partner about getting a laptop. This out-loud-thinking reminded her she probably still had her old university MacBook lying around, and after a quick search we found it and got me set up.</p>
<p>Right now I&#39;m typing away on this 2017 MacBook Pro with a 2.3 GHz Dual-Core Intel Core i5 and 8GB of RAM! It&#39;s stuck on Ventura, so I actually had to check out MacPorts for the first time since Homebrew didn&#39;t work, and it&#39;s actually quite nice. The heat this laptop puts out does suck though and the battery is terrible - but surprisingly I seem to be mostly CPU limited, not RAM limited... maybe I do get the pink MacBook Neo after all....</p>
<p>I did reach out to a friend who works at a big-box store to see if they had any old display units going into the &quot;For Staff&quot; pile, she&#39;s said there might be some M2 MacBook Air&#39;s dropping in. If they&#39;re under $1000<sup><a id="footnote-ref-1" href="#footnote-1" data-footnote-ref aria-describedby="footnote-label">1</a></sup> I&#39;ll definitely jump on it! But genuinely I kinda am surprised how usable this older MacBook Pro is, and how the RAM hasn&#39;t really made me feel like I&#39;m missing anything! Makes me think!</p>
<p>Other than the Laptop, we did try to get out of the house on the weekend and go window shopping. I&#39;ve got no money that <a class=""  href="../../thoughts/expenses.html">isn't already allocated</a>, so I didn&#39;t plan on buying anything... that was until Alice took me into Kaika - since she was surprised I&#39;d never seen it before! $44 later, I&#39;m now the proud owner of a <a class="externalLink" target="_blank" href="https://myfigurecollection.net/item/2689766">Miku Paint Girl</a> figurine<sup><a id="footnote-ref-2" href="#footnote-2" data-footnote-ref aria-describedby="footnote-label">2</a></sup>.</p>
<p>I&#39;ve also started playing some Shmups more, specifically <a class=""  href="../../games/deathsmiles.html">Deathsmiles</a> and DoDonPachi DaiOuJou. I&#39;ve got a crazy <a class="externalLink" target="_blank" href="https://bsky.app/profile/pfy.ch/post/3mgjlt23sh22a">doohickey setup</a> with my MCon controller to play vertical shmups in bed, and it&#39;s been fun to play more casually. I do wish the ROM check was a bit faster (or even skippable) in emulators. Deathsmiles is a lot harder than I remember, but it&#39;s still super fun. </p>
<p>It&#39;s my Birthday this week - so hoping next week&#39;s update will be a bit more action packed. I tried to plan something with friends but everyone just eyed my partner and were like &quot;uhhhhh yeahhhhh&quot; so I think she&#39;s planned something for me! I&#39;m excited :3c</p>
<p><bsky-comments data-post-id="3mgmm4infuk2k"></bsky-comments></p>
<section class="footnotes" data-footnotes>
<h2 id="footnote-label" class="sr-only">Footnotes</h2>
<ol>
<li id="footnote-1">
<p>Australian Dollars btw <a href="#footnote-ref-1" data-footnote-backref aria-label="Back to reference 1">↩</a></p>
</li>
<li id="footnote-2">
<p>Me if I kept doing Urbex post HRT <a href="#footnote-ref-2" data-footnote-backref aria-label="Back to reference 2">↩</a></p>
</li>
</ol>
</section>
 ]]></content>
                </entry><entry>
                  <id>https://pfy.ch/weekly/2026/09.html</id>
                  <title>Shopping!</title>
                  <published>2026-03-02T07:41:16.295Z</published>
                  
                  <link>https://pfy.ch/weekly/2026/09.html</link>
                  <emoji>📅</emoji>
                  <content type="html" xml:base="https://pfy.ch/weekly/2026/09.html"><![CDATA[ <p>This week has been fun - <a class=""  href="../../music/concerts/merzbow.html">I went to a concert</a>, Went shopping with a friend, and had a chill weekend just hanging out. The concert was really good, but I wrote about it entirely in its own post. I had a lot to say and knew it wouldn&#39;t all fit in my weekly post lol.</p>
<p>On Thursday, I took a short day at work by stacking my hours up into the other days - After lunch I clocked off and went into the city to go shopping with my partner and one of our friends. The goal was simple, at least TWO outfits. Right now I have like, just a few outfits, and I lack a lot of basics - so it&#39;s hard to put something together. I wanted to change this so we booked in this shop. We first hit Uniqlo, &amp; I picked up a blouse and some new pants. Then we went to Myer, and had zero luck finding anything that didn&#39;t give off bad white woman vibes<sup><a id="footnote-ref-1" href="#footnote-1" data-footnote-ref aria-describedby="footnote-label">1</a></sup>. Afterward we hit H&amp;M, and just picked up some reaaaallly basic tees with a nicer cut than my current ones, mostly to use as undershirts! </p>
<p>We ended up going back into Myer afterward since I made a comment about not having many comfy bras. My friend wanted me to try on and get my size for Calvin Klein, since she was telling me it was super comfy. I tried on a few bras and tops and found my size<sup><a id="footnote-ref-2" href="#footnote-2" data-footnote-ref aria-describedby="footnote-label">2</a></sup>, but we had to go to a different store to actually buy the things I wanted. I got a pyjamas top &amp; decided to order the rest online later, since I was already running my budget thin. After that we hit up target (lol) since we wanted to see if we could scrape the rest of my budget into some more basic things. Not much luck here since everything fit &amp; felt like shit, but we managed to get me some shorts!</p>
<p>We ended linking up with our friends boyfriend after this, and we went for dinner together. It had started to pelt  down rain while we were shopping so we had to trek to the restaurant trying to avoid getting super wet. We ended up getting Malaysian at a place called Sydney Kopitiam Cafe, since my friend had been going there with her family since she was little and was craving it. It was <em>really fucking good</em>. Apparently it&#39;s usually impossible to get a table, but the combo of pouring rain and random weekday meant that it was just us there<sup><a id="footnote-ref-3" href="#footnote-3" data-footnote-ref aria-describedby="footnote-label">3</a></sup>! My partner and I are suckers for small places with good family vibes so this has 100% been added to our eating out rotation.</p>
<p>The rest of the week was relatively chill - on Saturday we joined some friends at a Mardi Gras themed spin class before going and getting brunch together. Had the most fucked up yummy French toast to share and it went so hard, the coffee was also insanely good too!</p>
<p>It&#39;s kind of crazy how we&#39;re already into March. It&#39;s my birthday next week<sup><a id="footnote-ref-4" href="#footnote-4" data-footnote-ref aria-describedby="footnote-label">4</a></sup> &amp; my partner and I go to Europe together in April... This year&#39;s going so quick!</p>
<section class="footnotes" data-footnotes>
<h2 id="footnote-label" class="sr-only">Footnotes</h2>
<ol>
<li id="footnote-1">
<p>&quot;But Pfych! You&#39;re a white woman?!&quot; - I know, I&#39;m sorry <a href="#footnote-ref-1" data-footnote-backref aria-label="Back to reference 1">↩</a></p>
</li>
<li id="footnote-2">
<p>14C btw ٩(^ᗜ^ )و  <a href="#footnote-ref-2" data-footnote-backref aria-label="Back to reference 2">↩</a></p>
</li>
<li id="footnote-3">
<p>It also seemed like the restaurants entire family were there too for lunar new year dinner &amp; celebrations, the vibe was good! <a href="#footnote-ref-3" data-footnote-backref aria-label="Back to reference 3">↩</a></p>
</li>
<li id="footnote-4">
<p>Turning 25 on the 14th,,,, (๑╹ω╹๑) ｡ <a href="#footnote-ref-4" data-footnote-backref aria-label="Back to reference 4">↩</a></p>
</li>
</ol>
</section>
 ]]></content>
                </entry><entry>
                  <id>https://pfy.ch/music/concerts/merzbow.html</id>
                  <title>Lawrence English &amp; Merzbow</title>
                  <published>2026-02-27T04:17:01.580Z</published>
                  
                  <link>https://pfy.ch/music/concerts/merzbow.html</link>
                  <emoji>🎵</emoji>
                  <content type="html" xml:base="https://pfy.ch/music/concerts/merzbow.html"><![CDATA[ <p>This Wednesday I took my partner to go and see Merzbow live in Sydney. She&#39;d never listened to him before, and I&#39;d consider myself a casual enjoyer, so I thought it&#39;d be a fun night. I was honestly just interested to see the crowd, as well as see what the show would <em>actually look like.</em></p>
<p>Doors opened at 7pm, with an opening act played by Lawrence English, and then Merzbow. The show was at Oxford Art Factory, a small venue in Sydney, but one with good vibes - especially for a show like this. The venue was surprisingly busy, but we managed to get in right as doors opened and secure a seat up the back on the brick wall. It wasn&#39;t until I went to get a beer that I realized the entire venue was packed shoulder to shoulder - Something I&#39;d surprisingly not seen here before.</p>
<p> Lawrence started the show by coming out on stage, chatting with the crowd a little bit, and reflecting on previous shows, before starting his set. He emphasised how the show is about the people around you, and the shared experience, which I really resonated with. I was actually unfamiliar with his work, but I&#39;m glad I got to experience it live - the ambience was great, the low rumbles and resonance of the room made his whole set <em>an experience</em> that I was sharing with everyone else. Closing my eyes, looking down at my feet, staring into the dimly lit crowd - it all set a mood to think. I <strong><em>really</em></strong> enjoyed it, &amp; so did my partner!</p>
<p><img src="https://assets.pfy.ch/md/IMG_0803.jpg" alt="iPhone camera sux!" /></p>
<p>There was a brief intermission after Lawrence&#39;s set before Merzbow came out. We grabbed another beer, and chatted about how we really enjoyed the soundscape and &quot;journey&quot; that set had taken us on - visualizing things in our heads, feeling the music, just - taking in the vibe. While Lawrence was playing, there was very minimal lighting, just the dull red ambient lights of the venue. Perfect for just, zoning out. It all meshed as an experience - and I think that&#39;s what made it so good.</p>
<p>This set was a stark contrast to Merzbow, I knew the music would be more abrasive &amp; confronting. While Lawrence spent most of his set with bassy rumbles and ghostly overtones - Merzbow started with harsh, gritty, white noise. A whole different experience, but one that I was still excited to feel.</p>
<p>However, there was something different, that honestly - kinda took me out of it. Instead of low light, or even some strobes - there was a cheap projector projecting video over Merzbow onto the wall behind him. At first, I really paid no attention to it, but it was weirdly hollow - it was all looping clips, ken burns, <em>weird looking</em> CG renders. Like, at a glance they&#39;re unremarkable, but after seeing the same maybe, 10 clips rotate... it started to look off. They weren&#39;t hypnotic, abrasive, loud, they weren&#39;t ... well, really anything. That&#39;s when I started thinking - fuck, are these visuals all AI generated?</p>
<p>They had no soul, they all had the exact same duration, and they all ... felt flat. I&#39;m always down for fucked, weird, visuals during a show - like damn, even just white noise would have had more &quot;soul&quot;. But instead what we had were these weirdly unrelated, kinda clashing 3D renders playing behind him. It made it really difficult to zone out, and enjoy the vibe. Skill issue? I don&#39;t know. But coming from the <strong>great</strong> opening act from Lawrence, Merzbow somehow didn&#39;t clear the bar he set, and I was left disappointed. The music was good, it was what I was there for. But the set just didn&#39;t come together as a cohesive <em>experience</em>. Which I feel like is critical for this genre.</p>
<p>Afterward, my partner and I walked around the city and talked about the show. We talked a lot about pushing yourself to engage with &quot;difficult art&quot; or, art that you wouldn&#39;t consume normally. She also had the same feelings as me - Lawrence was great, but the disconnect of Merzbow&#39;s gritty, real and harsh music, with soulless AI visuals felt <em>wrong</em>, it wasn&#39;t just a &quot;difficult show&quot; - it was just &quot;bad&quot;. </p>
<p>It&#39;s funny saying a noise set lacked cohesion, but that&#39;s exactly how it felt. I&#39;m 100% certain I&#39;d feel differently if he just performed the same set to a dark room.</p>
<p><bsky-comments data-post-id="3mfsubrzl5s25"></bsky-comments></p>
 ]]></content>
                </entry><entry>
                  <id>https://pfy.ch/weekly/2026/08.html</id>
                  <title>Tidying &amp; Introspection</title>
                  <published>2026-02-23T01:25:37.702Z</published>
                  
                  <link>https://pfy.ch/weekly/2026/08.html</link>
                  <emoji>📅</emoji>
                  <content type="html" xml:base="https://pfy.ch/weekly/2026/08.html"><![CDATA[ <p>Not every week can be super interesting. Really the main thing that happened this week was my partner and I managed to tidy up a bunch of moving boxes that have been lingering in our study for 1.5 years. Consolidated everything down into a few tubs and freed up heaps of space for us to actually use the study for things now. We&#39;re thinking we might put a desk in there and move the 3D printer there, just so it&#39;s a bit more of a temperature controlled room etc.</p>
<p>Otherwise, really not that busy! I did end up creating an 88x31 badge for my site visible on the homepage or on the <a class=""  href="../../badge.html">badges</a> page. I also added some badges for sites I think are cool or sites run by people I&#39;ve yapped with.</p>
<p>On Yapping, I ended up yapping <em>heaps</em> with <a class="externalLink" target="_blank" href="https://aria.coffee">aria.coffee</a> on signal. I ended up liking some of her random posts on bsky, she liked some of mine, we both realised we&#39;re Australian, and it just kinda went from there<sup><a id="footnote-ref-1" href="#footnote-1" data-footnote-ref aria-describedby="footnote-label">1</a></sup>!</p>
<p>It made me realise that I&#39;m actually really missing having a cohort of friends I can yap randomly with about tech who are local to me. Like, someone pointed out this week how bsky is suffering from the &quot;linkedinification&quot; of discourse. It all needs to be about &quot;What the next big moves are&quot;, &quot;How can we scale&quot;, &quot;What are other players in the space talking about&quot; etc. </p>
<p>Originally this point was about bsky but reflecting on my conversations with coworkers, and other ppl I&#39;ve worked with etc, it&#39;s all like this. I&#39;m not saying I <em>don&#39;t</em> enjoy chatting with them. But I&#39;ve realised that I actually yearn for standard friendly banter with people that&#39;s tech adjacent<sup><a id="footnote-ref-2" href="#footnote-2" data-footnote-ref aria-describedby="footnote-label">2</a></sup>.</p>
<p>I do already have a space with friends online where I can talk like this, but it&#39;s really refreshing to have people in the same timezone as me (or close to me) where we can just... yap. IDK, maybe I&#39;m just yearning for trans community or something, people who are like me <strong>and</strong> have shared interests. For ages this was really just my partner, so I&#39;m kind of excited to try to get out a bit more and interact with more ppl.</p>
<p>I&#39;ve got some fun stuff planned for this week though so I&#39;m excited for that!</p>
<section class="footnotes" data-footnotes>
<h2 id="footnote-label" class="sr-only">Footnotes</h2>
<ol>
<li id="footnote-1">
<p>She also called me pretty, and I rode that high for like 3 days LOL <a href="#footnote-ref-1" data-footnote-backref aria-label="Back to reference 1">↩</a></p>
</li>
<li id="footnote-2">
<p>I have no idea if this makes any sense or if I&#39;m just rambling <a href="#footnote-ref-2" data-footnote-backref aria-label="Back to reference 2">↩</a></p>
</li>
</ol>
</section>
 ]]></content>
                </entry><entry>
                  <id>https://pfy.ch/weekly/2026/07.html</id>
                  <title>Lets just do these on Monday</title>
                  <published>2026-02-16T01:40:21.465Z</published>
                  
                  <link>https://pfy.ch/weekly/2026/07.html</link>
                  <emoji>📅</emoji>
                  <content type="html" xml:base="https://pfy.ch/weekly/2026/07.html"><![CDATA[ <p>I really like the fact that I&#39;m reflecting every week on what I&#39;ve done - it&#39;s been a great way to make sure I don&#39;t forget shit. I initially planned to post every Sunday, but I&#39;ve found that I&#39;ve been actually <em>doing</em> stuff lately on Sundays which makes blogging that day actually really hard. Moving forward I&#39;m going to try do these posts on Monday! Mondays are almost always slow &amp; I&#39;m just working, so it&#39;s the perfect day to write about the last week.</p>
<p>Last week I finally got the Azur lane figurine I ordered <a class=""  href="./03.html">4 weeks ago</a>! The box was actually HUGE and the figurine doesn&#39;t even fit on our shelf ;-; ... She&#39;s SOOOOOO pretty though, and I&#39;m glad I finally got her. I&#39;d fucked up the post code on my delivery address so it got sent literally everywhere except my suburb. Shoutout to the AusPost phone support honestly, once you get past the shit bots the people I chatted too were super understanding and explained how the new label is getting printed &amp; what the tracking would look like etc. All really great friendly ppl!</p>
<p><img src="https://assets.pfy.ch/md/DSCF0878.jpg" alt="" /></p>
<p>Other than that and Sunday though my week was a bit uneventful! Sunday was crazy though - I had a friend basically disappear over the last few months, like she was still around, but never free. Turns out she was scouted to be an idol. Like the japanese idol kind. She invited us all to one of her shows and she was <em><strong>crazy</strong></em> good (and so was her other group member!) - it&#39;s kinda crazy tho that there&#39;s a scene for this in Sydney. Like, it makes sense, but its cool that niche communities like this can form and be big enough that it can actually afford to pay her and the other girls who perform. Was really cool!</p>
<p>Oh! Also, my NAS that <a class=""  href="./06.html">died last week</a> is all fixed! New drives arrived at the same time as the figurine &amp; I swapped them in. The 8hr resilver was stressful, but it all went well in the end. Trying to orchestrate a RaidZ2 migration in my head without dropping crazy $$$ though, but I don&#39;t think it&#39;ll be possible. Glad I had good backups, but I dread having to restore them...</p>
 ]]></content>
                </entry><entry>
                  <id>https://pfy.ch/weekly/2026/06.html</id>
                  <title>Late Again, with a dead NAS</title>
                  <published>2026-02-09T00:48:31.878Z</published>
                  
                  <link>https://pfy.ch/weekly/2026/06.html</link>
                  <emoji>📅</emoji>
                  <content type="html" xml:base="https://pfy.ch/weekly/2026/06.html"><![CDATA[ <p>This week was another lazy one, Keeping it chill and not really getting up too much. I managed to slot one run in this week. Where my partner and I did 3.5km @ 6:50 pace - I definitely struggled with my stamina on this one having to stop for a quick 30s breather every Km. But I did it, so that was good!</p>
<p>Otherwise, the main big thing that happened this week was a disk dying in my NAS RaidZ1 array! Completely out of nowhere, the controller board died &amp; it refuses to do really anything. I ordered 2x replacement drives straight away, 1 to drop in and the other to keep as a cold spare from Server Part Deals and ended up paying almost 2-3x what I paid late 2024! CRAZY! I&#39;ve currently got the NAS powered off while I wait for the replacement drive (that should hopefully come today), I don&#39;t want to risk complete data loss. I did run a few final backups of things before shutting it all down, and it did highlight some things that I didn&#39;t have automatically backing up correctly which was nice.</p>
<p>I migrated my ATProto PDS to my VPS so I could continue to use bsky with the NAS down and spent ages fiddling with what I thought was file corruption, when it was instead actually just the remote encryption I set up ... glad to know that all works!</p>
<p>Socially I had some friends over for a dinner and to hang out, we watched some YouTube mostly since my NAS was dead, but we did spend a HEAP of time browsing <a class="externalLink" target="_blank" href="https://neocities.org">neocities</a> and <a class="externalLink" target="_blank" href="https://nekoweb.org/">nekoweb</a> for other peoples personal webpages. SOOOO much sauce on some of those sites, and we came up with a cool list of changes I can make to my site to make it less static/flat/booring while still staying readable. Stay posted for those updates!</p>
<p>That&#39;s really it for this week - really hoping my figurine I ordered <a class=""  href="./03.html">3 weeks ago</a> shows up today, its meant too. Would be awesome to have it and the new drives rock up together!</p>
 ]]></content>
                </entry><entry>
                  <id>https://pfy.ch/weekly/2026/05.html</id>
                  <title>Beating the heat</title>
                  <published>2026-02-01T03:30:07.092Z</published>
                  
                  <link>https://pfy.ch/weekly/2026/05.html</link>
                  <emoji>📅</emoji>
                  <content type="html" xml:base="https://pfy.ch/weekly/2026/05.html"><![CDATA[ <p>I knew that doing weekly blog posts might be a little difficult - since not every week I really get up to too much. But it really does help me reflect on &quot;nothing&quot; weeks to try pull out and think about what I actually got up too.</p>
<p>I started my week getting two new estrogen implants put in - my levels and bloods look good, and I&#39;m feeling great! Even if the implants are a bit pricey ($240 for two) vs other methods like injections or gels - The convenience factor makes up for it entirely. Honestly my main inconvenience medication wise now is actually making it out to the Dr who puts them in my cheeks lol. It is something I would like to <em>maybe</em> DIY someday.</p>
<p>Wednesday I actually went into the city and got a few arcade credits in for the first time in a while! I&#39;ve been mostly playing <a class=""  href="../../games/bms.html">BMS</a> at home, but last week I talked about my friend who&#39;s moving to Perth, and we wanted to get one more set of <a class=""  href="../../games/iidx/iidx.html">IIDX</a> in before he left. After playing some games at the arcade we went to Ume Burger @ Darling Quarter and had burgers and fries for dinner. Nothing crazy, but imo - Ume Burger does the best burger in Sydney.</p>
<p>On Friday - my MCON finally arrived after backing it on Kickstarter like a year ago. It was kind of a shit-show fulfillment wise, but it&#39;s nice to finally have it in my hands. Sadly - now that it&#39;s in my hands, it feels like shit to use. I mostly game stream from bed with my GameSir G8 or Steam Deck and was hoping that this could be a compact little controller I could keep on me to play games. But instead, when using both sticks and triggers its super crampy and small! Really is a shame, apparently it&#39;s going to be fixed in V2, but I really don&#39;t want to give OhSnap my money again due to how fucked messy the whole shipping internationally thing was. It&#39;s fine for D-Pad and face-button gaming though, so I&#39;ll still be using it. But I&#39;m going to try to keep mine in its box in nice shape since I got the Kickstarter exclusive translucent colour. Hopefully someone who <em>does</em> like the controller will pay 2x what I paid ($99 USD) sometime in the future haha.</p>
<p>That same day my brother came over to avoid the heat &amp; try out my sim racing rig. It&#39;s nothing fancy, just a Moza R5 combo with a clutch, but it&#39;s still a blast to drive. I&#39;ve really got to get a base to screw it all into because it slides around a lot when playing, especially with the load cell brakes upgrade! The next day, my partner and I drove up to my parents place to catch up, have a few drinks and have pizza. I&#39;ve been craving this pizza from the pizza place nearby for like 2 months now - nothing here in Sydney hits the spot the same. I went full fat-fuck mode (It&#39;s a mindset) and ate nearly a kilo of pizza that night. Went hard, and I felt like shit the next day but was 100% worth it.</p>
<p>I did get some exercise in this week though. Not as much as I would have liked, but I did a spin class this morning. Absolute hell after a massive pizza dinner - but was still fun!</p>
<p>Otherwise, that&#39;s about it! I&#39;ve been playing heaps more Warframe as usual too - I got my first prime this week which is fun! Excited to keep grinding and enjoying the game :3c</p>
 ]]></content>
                </entry><entry>
                  <id>https://pfy.ch/weekly/2026/04.html</id>
                  <title>Long Weekend</title>
                  <published>2026-01-25T23:58:32.844Z</published>
                  
                  <link>https://pfy.ch/weekly/2026/04.html</link>
                  <emoji>📅</emoji>
                  <content type="html" xml:base="https://pfy.ch/weekly/2026/04.html"><![CDATA[ <p>I almost forgot to post! Technically this is still the 4th week weekend (since we have a long weekend here today)!</p>
<p>This week has really been quite chill - lots of not much. I managed to fit <strong><em>two</em></strong> spin classes <em>and</em> a run in! Honestly it&#39;s how every week should be - but I&#39;m glad I got in all in.</p>
<p>Otherwise, work was fun this week. We did an offsite at The Pillars club in one of their boardrooms - was crazy swanky &amp; was fun, having staff keep my coffee topped up was dangerous but really nice. We did lots of just yapping about the company and planning for the new year. Excited for the push this year &amp; keen to work on heaps of cool stuff. After all that work we had some drinks in the clubs bar and yapped about movies. I&#39;m constantly reminded that I&#39;m behind and haven&#39;t watched so many good movies. Someday ... I&#39;ll binge &quot;the classics&quot; - but not any time soon lol.</p>
<p>On watching things - my partner and I finished watching <a class="externalLink" target="_blank" href="https://anilist.co/anime/180929/Ruri-Rocks/">Ruri Rocks</a> this week. It&#39;s a show about a highschooler getting into geology with the help of two Uni students. It&#39;s a really pretty show &amp; it made geology actually interesting lol! Was cool to learn about minerals from such a cozy show :3c.</p>
<p>On Saturday I went to a farewell dinner for one of my friends who&#39;s moving to Perth for Med School - We went to <a class="externalLink" target="_blank" href="https://web.archive.org/web/20260125235228/https://dukinn.com.au/">Duk Inn</a> which was a weird funky restaurant that&#39;s hard to describe... The menu had things like &quot;Experimental beef cubes&quot; and &quot;Mystery ribs? (Order them)&quot;. It was all very very yummy though. As much as it sucks to have to move to Perth - I&#39;m really excited for my friend and I hope he does well at Med School! My partner and I will have to come visit :3c</p>
<p>Next week is a &quot;short week&quot; since this Monday is part of the long weekend. Hoping that I can slot in some shopping at some point - I&#39;ve really got to get some more business-casual summer outfits ... I can only get away with rotating the same 2 outfits in the office for so long!</p>
 ]]></content>
                </entry><entry>
                  <id>https://pfy.ch/weekly/2026/03.html</id>
                  <title>DIY &amp; Rainy Weekend</title>
                  <published>2026-01-18T04:37:09.297Z</published>
                  
                  <link>https://pfy.ch/weekly/2026/03.html</link>
                  <emoji>📅</emoji>
                  <content type="html" xml:base="https://pfy.ch/weekly/2026/03.html"><![CDATA[ <p>It has been a bit of a miserable weekend, <em>super</em> stormy and pouring down rain. However! I&#39;ve still managed to have a pretty good week!</p>
<p>Work wise - I&#39;ve mostly spent the entire week planning, researching and building a proof of concept project that&#39;s pretty cool. It&#39;s nothing user-facing, so I can&#39;t share details, but I&#39;m enjoying the challenge it brings. I&#39;m hoping to have something production ready by late next week but time will tell :3c. We also had a new starter at the company I work at on Wednesday! </p>
<p>It&#39;s actually someone I&#39;ve worked with in the past. It just so happened we ended up catching up for drinks, I mentioned that I was stressed hiring, which he mentioned being stressed job hunting haha. I didn&#39;t end up interviewing him since I didn&#39;t want to skew the teams opinions, but he got along well with the team and passed the interview pretty well! I&#39;m excited to continue working with him.</p>
<p>Other than work I actually did some cool things with friends this week. One of my friends works at a store that houses an e-waste drop-off point. Often cool things come through, and she&#39;ll grab them (since most things still work). But this time she managed to snag an old stereo/radio/CD player thing. It&#39;s nothing crazy but the vibes it radiates made it worth the snag. Sadly one of the speakers was <em>really</em> quiet, and when opening it up, she had literally no idea what to do.</p>
<p>She sent me a message with pictures of it&#39;s guts and asked if I knew how to fix stereos, and I lied and said yes!<sup><a id="footnote-ref-1" href="#footnote-1" data-footnote-ref aria-describedby="footnote-label">1</a></sup> We organised an &quot;e-waste pizza party&quot; at my place and her and her partner (one of my rlly close friends) came over, and we got to it. We gave the whole thing a good clean and scrub - lots of corrosion from batteries on the inside, but no real visible damage. However - just the clean seemed to make the quiet speaker <em>slightly</em> louder.</p>
<p>I poked and prodded around for a little bit but couldn&#39;t pinpoint the exact issue. Which is when I tried a hail-mary. &quot;The right side speaker works right? But the left doesn&#39;t? Are you cool with no left channel?&quot;. She was. So I daisy-chained the right speaker to the left and it worked! We played a few CDs, and it all sounded fine (if you don&#39;t mind missing out the left channel :3c).</p>
<p>After fixing the player we ordered pizza and sat around the TV while we scrolled AmiAmi looking at anime figurines. During the repair, I&#39;d mentioned that I&#39;m a little bummed I never caved and bought my own cool big figurine like my partner has (multiple times :3c), and that I&#39;d like a big figure of my own in the cabinet that lives in the living room. We ended up pulling up a figurine I&#39;d actually wanted but passed on before ... <a class="externalLink" target="_blank" href="https://www.amiami.com/eng/detail/?scode=FIGURE-156662">a figurine of New Jersey from Azur Lane in her Oath skin</a>. </p>
<p>Being surrounded by friends - and a lot of back and forth umming and ahhing about <a class=""  href="../../thoughts/expenses.html">budget</a> etc... I ended up pulling the trigger. Since it was on sale for <em>only</em> ¥28,000 down from its retail ¥43,780 (What a steal!). However, if you&#39;ve shopped with AmiAmi before you&#39;re likely aware of the issue here - <strong><em>shipping</em></strong>. After placing the order, a few days later I was forwarded the quote for shipping. ¥17,410 with EMS or <em><strong>¥42,100 with DHL</strong></em>... Needless to say, I gritted my teeth and selected the EMS option. Making the figurine come out to ¥49,951 in total. </p>
<p>I think it&#39;s worth it though. She&#39;s one of my favourite ships from Azur &amp; I&#39;ve wanted the figurine in the past (I&#39;m treating myself<sup><a id="footnote-ref-2" href="#footnote-2" data-footnote-ref aria-describedby="footnote-label">2</a></sup>).</p>
<p>Other than all that though, the week has been a bit slow - my partner and I spent the weekend indoors avoiding the storms &amp; working on world-building/lore for a future project! I&#39;m excited to share more details eventually... but not right now!</p>
<section class="footnotes" data-footnotes>
<h2 id="footnote-label" class="sr-only">Footnotes</h2>
<ol>
<li id="footnote-1">
<p>Well... I made a guess that sounded right, and she trusted me to give it a shot <a href="#footnote-ref-1" data-footnote-backref aria-label="Back to reference 1">↩</a></p>
</li>
<li id="footnote-2">
<p>And other things you can say to make a ¥49,951 purchase hurt less  <a href="#footnote-ref-2" data-footnote-backref aria-label="Back to reference 2">↩</a></p>
</li>
</ol>
</section>
 ]]></content>
                </entry><entry>
                  <id>https://pfy.ch/weekly/2026/02.html</id>
                  <title>Back to work</title>
                  <published>2026-01-11T01:19:37.407Z</published>
                  
                  <link>https://pfy.ch/weekly/2026/02.html</link>
                  <emoji>📅</emoji>
                  <content type="html" xml:base="https://pfy.ch/weekly/2026/02.html"><![CDATA[ <p>This is hopefully the first of many blog posts. As I mentioned in my <a class=""  href="../../thoughts/2025-retrospective.html">2025 Year in review</a>, I&#39;d like to try blogging weekly. Just really briefly about what I did, how I felt, and things I wanted to write down. Mostly as a self-reflection thing, but also to share what I&#39;ve been up too.</p>
<p>This week was my first week back working. I&#39;m glad I had time off over the holidays - but it&#39;s nice to get back into my routine of getting things done at work. Nothing too crazy going on career wise, spent some time researching workflow engines and messing with AI. Hopefully can share more in the future!</p>
<p><strong>Personal life wise</strong> - this was the first week when I really tried to stay on top of my medications, actually taking them &amp; following my phone alerts. My latest ADHD medication gives me a little bit of Nausea, but it&#39;s not as bad as the last one which made me really grumpy and depressed. So we call that a win! Still on health, I caught up with my Physio - They&#39;ve changed due to some merger whatevers, so I was a little worried, but they were good! Getting back onto those exercises this week since I was <em>really</em> lazy during my time off haha.</p>
<p>I managed to get three runs in this week as well, trying to stick to a routine with my partner on this... I set a record today for longest running workout (4.47km) and General running record (1,335Kj) today - it was quite cold after an insanely hot day yesterday so I feel like the cool breeze and overcast conditions helped me not gas myself out as quickly as usual. It felt good! Good splits &amp; pace too - I&#39;ve been doing 2:1 splits with a target of 6:20, and I&#39;m definitely getting better at holding pace. As expected though - I do fall off at the end lol.</p>
<p>I also got my hair done this week - I saw my usual stylist in Marrickville, was good to yap and get my hair tidied up a little. Went back to the curly bangs instead of the side-part I&#39;d had for a while (mostly due to me being lazy about maintaining the bangs lol). Was informed that due to my blonde hair that I need to worry about my hair bleaching in the sun!! Apparently it&#39;s nothing too bad as long as I use a good conditioner and care for my hair properly (which I will now). Also had a catchup with my Psychologist this week, and she complimented my hair which was nice! We discussed a lot of stuff regarding discipline &amp; fear of &quot;wasting time&quot; and where that all stems from - was very productive!</p>
<p><strong>Tech wise</strong> - my friends and I have started using IRC a little bit. I set up ZNC &amp; ErgoChat on my NAS for a bit to tinker and then migrated them to my VPS for better reliability. It&#39;s quite unintuitive how it all works, but we&#39;ve had fun so far &amp; discussions on IRC seem to be a lot more interesting and productive than chats on discord or other platforms. We&#39;re still figuring it out, I&#39;m interested to see how long we stick to it before dropping off haha.</p>
<p>I also watched my partner beat <a class="externalLink" target="_blank" href="https://store.steampowered.com/app/2592160/Dispatch/">Dispatch</a> a few nights ago - I really enjoyed the writing and story, and I&#39;m happy with the ending we got! We played it over a couple nights, doing 2 episodes after dinner like we normally do with TV shows. Highly recommend. Otherwise, for games - I mostly just played <a class="externalLink" target="_blank" href="https://store.steampowered.com/app/230410/Warframe/">Warframe</a>. It&#39;s hooked us both lately, and we&#39;re just grinding through the base star-chart &amp; campaign. I&#39;m playing on my old account back from when I was in High school with my Xbox One - it&#39;s crazy because I remember playings HOURS with friends after school but logging back in all I have are a bunch of frames a friend bought us, a massive dojo, and like ZERO campaign progress LOL. No clue what we were up too lol.</p>
 ]]></content>
                </entry><entry>
                  <id>https://pfy.ch/thoughts/2025-retrospective.html</id>
                  <title>Looking back on 2025</title>
                  <published>2026-01-05T05:22:57.010Z</published>
                  
                  <link>https://pfy.ch/thoughts/2025-retrospective.html</link>
                  <emoji>🤔</emoji>
                  <content type="html" xml:base="https://pfy.ch/thoughts/2025-retrospective.html"><![CDATA[ <style>
    p:has(img) {
        display: flex;
        flex-wrap: wrap;
    }
    
    p:has(img) img {
        flex: 1 1 50%;
        min-width: 300px;
    }
</style>

<p>It&#39;s now 2026, which is crazy to think about. The <a class="externalLink" target="_blank" href="https://web.archive.org/web/20230329141710/https://pfy.ch/blog/21-12-15.html">oldest post</a> on this blog is from late 2021, making this site ~4 years old. I want to start blogging more here, consistently. Like yes, I did make ~16 posts, but ideally I&#39;d like to have more general updates, maybe weekly. Mostly for my own sake - since I really struggled to do this retrospective lol, I had to dig through my phones camera roll for the year to remind myself of things I&#39;ve done.</p>
<p>I was originally going to write some grand big long post, but it&#39;s a bit of a hodgepodge, so I&#39;ve instead done whatever this is :3c</p>
<p><img src="https://assets.pfy.ch/sm/DSCF0718.jpg" alt="" />
<img src="https://assets.pfy.ch/sm/DSCF0601.jpg" alt="" /></p>
<h2 id="places-i-went-in-2025">Places I went in 2025</h2>
<ul>
<li>I traveled to Melbourne for my Partners birthday. I&#39;d never been, so it was really great staying in the CBD &amp; enjoying heaps of good food &amp; drink. I definitely fell in love with the city &amp; I would love to live there someday.</li>
</ul>
<p><img src="https://assets.pfy.ch/sm/DSCF0574.jpg" alt="" /></p>
<h2 id="small-wins-in-2025">Small wins in 2025</h2>
<ul>
<li>I really feel like I started spending time and chatting with more people in 2025, being a bit more outgoing and trying to connect. Less of a shut-in outside of work. I want to keep this going! It is exhausting constantly being the one to organise things IRL but its worth the hassle and exhaustion when I see people I care about having fun and hanging out :3c</li>
<li>I also started focusing on my health more - I picked up running and cycling, and I&#39;ve started seeing a physio. Again - my poor discipline kills me with staying up on these, but I&#39;m really trying to push myself to do <em>something</em> since it&#39;s better than nothing. I&#39;m proud of some of the milestones I&#39;ve hit and I feel a lot better now that I am exercising more often. </li>
<li>I got really into Riichi Mahjong in 2025, starting at the beginning of the year. I haven&#39;t played as much as I&#39;d have liked to, but I&#39;ve tried to make it a habit to play with friends often. I&#39;ve found it really good for working on my focus, and trying to keep my mind sharp.</li>
</ul>
<p><img src="https://assets.pfy.ch/sm/DSCF0463.jpg" alt="" /></p>
<h2 id="big-changes-in-2025">Big Changes in 2025</h2>
<ul>
<li>I got a new job! After working in the same role at my previous employer for almost 6 years I&#39;m happy to have moved on to something new. The previous role was great - but I needed a change. I&#39;m excited to see where it takes me in the new year. </li>
<li>I feel like I&#39;ve hit a good point in my Transition where I&#39;m really happy with who I am. Everyone at my new role calls me by my new name &amp; I feel like I&#39;m slowly figuring out my style, fashion and general vibe. It still needs work, but I&#39;m really happy with my progress in 2025. </li>
<li>My partner and I got a cat! Her name is Cami &amp; we love her so much. She&#39;s a 9-month-old stray that a friend rescued, they think she may have been abandoned once they got past the &quot;cute kitten&quot; stage of their life. She&#39;s an absolute angel and a big baby who loves people &amp; enjoys napping. I&#39;m excited to care for her and give her the best life possible!</li>
<li>I finally got medicated for ADHD, initially with Dexamphetamine, now with Ritalin. Dexamphetamine made me grumpy, irritable and angry - which lead to me being depressed (because I didn&#39;t like to be angry). However, Side effects aside - I found it incredibly helpful for my focus and decision paralysis! Never being able to <em>start</em> anything was always my biggest hurdle, and I do really hope that I can <em><strong>start</strong></em> more things in this new year</li>
</ul>
<p><img src="https://assets.pfy.ch/sm/DSCF0455.jpg" alt="" /></p>
<h2 id="things-i-want-to-change-from-2025">Things I want to change from 2025</h2>
<ul>
<li>I didn&#39;t really take as many photos as I would have liked - Honestly I really started dropping photography in 2024, I used excuses like &quot;Oh I mostly take family and personal photo&#39;s now which is why I don&#39;t share them&quot;. But while party true was really an excuse I tricked myself into believing. I want to pick it up again, because I really did enjoy it. The photos in this post are random ones I took during 2025 that I have not shared.</li>
</ul>
<p><img src="https://assets.pfy.ch/sm/DSCF0753.jpg" alt="" /></p>
<h1 id="goals-for-2026">Goals for 2026</h1>
<ul>
<li>Run 10km at a pace I&#39;m proud of</li>
<li>Read at least 3 full books and write about them here</li>
<li>100% a game and write about it here</li>
<li>Hit credits in a game and write about it here</li>
<li>Skateboard again (I miss it, but I&#39;m also afraid)</li>
<li>Wear makeup consistently for a week (Laziness kills me, especially when I WFH. I need practice)</li>
<li>Get my nails done (I&#39;ve never done this before, and it looks like fun)</li>
<li>Cosplay at a convention with friends</li>
<li>Start work on a game project (Even if its shit and I don&#39;t show anyone)</li>
<li>Turn my expenses planner into an app</li>
<li>Create an OC or a Glorbo (I want to be more creative)</li>
<li>Learn to use a sewing machine</li>
<li>Clear SL2 in BMS</li>
<li>Ride front row at a spin class and not die</li>
<li>Try my best to blog weekly about what I&#39;ve got up too</li>
<li>Stream more</li>
<li>Stop telling people about project ideas that I haven&#39;t started</li>
</ul>
<p><img src="https://assets.pfy.ch/sm/DSCF0633.jpg" alt="" /></p>
 ]]></content>
                </entry><entry>
                  <id>https://pfy.ch/diy/alid-solar-christmas-lights.html</id>
                  <title>Repairing Aldi solar christmas lights</title>
                  <published>2025-12-09T01:59:17.926Z</published>
                  
                  <link>https://pfy.ch/diy/alid-solar-christmas-lights.html</link>
                  <emoji>📦</emoji>
                  <content type="html" xml:base="https://pfy.ch/diy/alid-solar-christmas-lights.html"><![CDATA[ <p>Specifically the colour version of <a class="externalLink" target="_blank" href="https://www.aldi.com.au/product/casalife-250-pack-led-solar-fairy-lights-000000000678134001">these</a> &quot;Casalife&quot; solar powered christmas lights (I&#39;m assuming they&#39;re all the same inside).</p>
<p>For $9.99 in the middle isle who wouldn&#39;t be tempted? I&#39;ve been fiddling with assorted solar projects so thought even if they broke instantly I&#39;ll at least get maybe a battery cell &amp; a dinky panel out of it!</p>
<p>They lasted about <em>2 days</em> before they&#39;d randomly start flickering on and off quickly and strobing our neighbors! It took me a while to finally get around to taking them down after turning them off (laziness etc.). But once they were down I opened them up and was surprised to see the solder connection for the LEDs to the control board was broken! Re-soldering this joint fixed the lights entirely.</p>
<p><img src="/guide-images/aldi/inside.jpg" alt="The inside of the solar box" /></p>
<p>Inside was a simple board with pads for the battery pack (1800mAh), the solar panel, &amp; pins for the basic LED strip. I don&#39;t own a working multimeter right now so I didn&#39;t look too deep into everything. Thats a project for next time!</p>
<p>Ideally I&#39;d like to put an ESP-32 inside here and have them be &quot;smart&quot; lights, but I lack the connectors to do it safely without soldering everything directly to the board. Might tackle this another day.</p>
<p><bsky-comments data-post-id="3m7jhpvod4s27"></bsky-comments></p>
 ]]></content>
                </entry><entry>
                  <id>https://pfy.ch/games/bms-tables.html</id>
                  <title>BMS Table Difficulties</title>
                  <published>2025-12-07T08:23:11.756Z</published>
                  <updated>2025-12-07T10:07:18.652Z</updated>
                  <link>https://pfy.ch/games/bms-tables.html</link>
                  <emoji>🎮</emoji>
                  <content type="html" xml:base="https://pfy.ch/games/bms-tables.html"><![CDATA[ <p>Mapping common <a class=""  href="./bms.html">BMS</a> table difficulties to each-other as reference.</p>
<p><bms-difficulties></bms-difficulties></p>
<p><bsky-comments data-post-id="3m7fcpveszc2q"></bsky-comments></p>
 ]]></content>
                </entry><entry>
                  <id>https://pfy.ch/programming/splitting-mcp-saves.html</id>
                  <title>Splitting .mc2 memory cards on Linux</title>
                  <published>2025-11-01T01:13:27.067Z</published>
                  
                  <link>https://pfy.ch/programming/splitting-mcp-saves.html</link>
                  <emoji>💻</emoji>
                  <content type="html" xml:base="https://pfy.ch/programming/splitting-mcp-saves.html"><![CDATA[ <p>I recently purchased a <a class="externalLink" target="_blank" href="https://8bitmods.com/memcard-pro2-for-ps2-and-ps1-smoke-black/">Memcard Pro2</a> for my PS2, it has a really cool feature where it can hotswap memory cards when games are loaded or when the Playstation 2 boots up.</p>
<p>I have a <a class="externalLink" target="_blank" href="https://github.com/israpps/FreeMcBoot-Installer/releases/tag/mcpro2-img">virtual memory card with FreeMcBoot</a> mount on startup and then when I launch a game either via OPL or with a Disc, the Memcard Pro2 automatically mounts the game specific memory card. However, I&#39;ve got an <em>actual</em> PS2 memory card with all my game saves and I had no idea how to go about transferring these over. The first thought was to create a virtual copy of my existing memory card: </p>
<ol>
<li>Create a new Virtual memory card on the Memcard Pro2</li>
<li>Insert both into your PS2</li>
<li>Use the PS2 memory card browser to copy the saves from the old memory card to the virtual memory card</li>
</ol>
<p>Now I&#39;ve got a <code>.mc2</code> file with all my saves on the Memcard Pro2&#39;s SD card! By using a program like <a class="externalLink" target="_blank" href="https://github.com/Adubbz/mymcplusplus">MyMC++</a> I can browse the memory card:</p>
<pre><code class="hljs language-bash"><span class="hljs-comment"># I use uv for python apps/packages since it makes managing them easier.</span>
<span class="hljs-comment"># This will take a while to launch initially while it installs!</span>
uvx -v <span class="hljs-string">&quot;mymcplusplus[gui]&quot;</span>
</code></pre><p>Using the MyMC++ GUI I can export each of the actual game saves off the <code>.mc2</code> file into individual <code>.psu</code> files using export button in the header.</p>
<p>Once exported I created the following script in the same directory:</p>
<pre><code class="hljs language-bash"><span class="hljs-meta">#!/bin/bash</span>

<span class="hljs-keyword">for</span> save <span class="hljs-keyword">in</span> *.psu; <span class="hljs-keyword">do</span>
  FILENAME=$(<span class="hljs-built_in">basename</span> -- <span class="hljs-string">&quot;<span class="hljs-variable">$save</span>&quot;</span>)
  NO_EXTENSION=<span class="hljs-variable">${FILENAME%.*}</span>
  NO_PREFIX=<span class="hljs-variable">${NO_EXTENSION#&quot;BI&quot;}</span>
  GAME_ID=$(<span class="hljs-built_in">echo</span> <span class="hljs-string">&quot;<span class="hljs-variable">$NO_PREFIX</span>&quot;</span> | <span class="hljs-built_in">cut</span> -f1 -d<span class="hljs-string">&quot; &quot;</span> | <span class="hljs-built_in">cut</span> -c1-10)

  MEMCARD_FOLDER=<span class="hljs-string">&quot;memcards/<span class="hljs-variable">$GAME_ID</span>&quot;</span>
  <span class="hljs-built_in">mkdir</span> -p <span class="hljs-string">&quot;<span class="hljs-variable">$MEMCARD_FOLDER</span>&quot;</span>

  MEMCARD=<span class="hljs-string">&quot;<span class="hljs-variable">$MEMCARD_FOLDER</span>/<span class="hljs-variable">$GAME_ID</span>-1.mc2&quot;</span>
  uvx mymcplusplus -i <span class="hljs-string">&quot;<span class="hljs-variable">$MEMCARD</span>&quot;</span> format
  uvx mymcplusplus <span class="hljs-string">&quot;<span class="hljs-variable">$MEMCARD</span>&quot;</span> import <span class="hljs-string">&quot;<span class="hljs-variable">$save</span>&quot;</span>
  curl -s <span class="hljs-string">&quot;https://raw.githubusercontent.com/niemasd/GameDB-PS2/refs/heads/main/games/<span class="hljs-variable">$GAME_ID</span>/title.txt&quot;</span> &gt;<span class="hljs-string">&quot;<span class="hljs-variable">$MEMCARD_FOLDER</span>/name.txt&quot;</span>

<span class="hljs-keyword">done</span>

<span class="hljs-built_in">echo</span> <span class="hljs-string">&quot;&quot;</span>
<span class="hljs-built_in">echo</span> <span class="hljs-string">&quot;Done!&quot;</span>
</code></pre><p>This script loops over each of the saves, pulls the <code>GAME_ID</code> from the file name, and then creates a dedicated <code>.mc2</code> folder &amp; file for the game. Ready to add directly to the Memcard Pro2!</p>
<p>Once copied onto the SD card the dir tree should look like this:</p>
<pre><code class="hljs">OS
PS1
PS2
├ SLPM-66621
│ ├ name.txt
│ └ SLPM-66621-1.mc2
└ SLPM-55117
  ├ name.txt
  └ SLPM-55117-1.mc2
</code></pre> ]]></content>
                </entry><entry>
                  <id>https://pfy.ch/games/iidx/price-credits.html</id>
                  <title>Arcade Pricing in Sydney</title>
                  <published>2025-07-05T07:38:43.284Z</published>
                  <updated>2025-07-15T07:38:27.000Z</updated>
                  <link>https://pfy.ch/games/iidx/price-credits.html</link>
                  <emoji>🎮</emoji>
                  <content type="html" xml:base="https://pfy.ch/games/iidx/price-credits.html"><![CDATA[ <p>With certain arcades offering bonuses the cost-per-play metric can sometimes be a bit confusing to calculate. This page lists the cost of some games at certain venues.</p>
<p>Since most venues offer &quot;Bonuses&quot;, costs can vary. For a breakdown of this math, check the &quot;The Math&quot; section below. </p>
<ul>
<li><a class=""  href="#the-venues">The Venues</a></li>
<li><a class=""  href="#the-games">The Games</a><ul>
<li><a class=""  href="#beatmania-iidx">Beatmania IIDX</a></li>
<li><a class=""  href="#sound-voltex">Sound Voltex</a></li>
<li><a class=""  href="#gitadora">Gitadora</a></li>
</ul>
</li>
<li><a class=""  href="#the-math">The Math</a></li>
</ul>
<h1 id="the-venues">The Venues</h1>
<p>The following venues offer deals where the &quot;Real Dollar Value&quot; can get quite low!</p>
<div class="sortable spreadsheet" data-name="venue-value">

<table>
<thead>
<tr>
<th>Venue</th>
<th>Top Up</th>
<th>Bonus</th>
<th>Total</th>
<th>Real Dollar Value</th>
</tr>
</thead>
<tbody><tr>
<td>Puri</td>
<td>$1</td>
<td>$0</td>
<td>= B1 C1 + $</td>
<td>= D1 B1 / ^ $</td>
</tr>
<tr>
<td>Koko</td>
<td>$1</td>
<td>$0</td>
<td>= B2 C2 + $</td>
<td>= D2 B2 / ^ $</td>
</tr>
<tr>
<td>Koko $30</td>
<td>$30</td>
<td>$10</td>
<td>= B3 C3 + $</td>
<td>= D3 B3 / ^ $</td>
</tr>
<tr>
<td>Koko $60</td>
<td>$60</td>
<td>$30</td>
<td>= B4 C4 + $</td>
<td>= D4 B4 / ^ $</td>
</tr>
<tr>
<td>Koko $100</td>
<td>$100</td>
<td>$60</td>
<td>= B5 C5 + $</td>
<td>= D5 B5 / ^ $</td>
</tr>
<tr>
<td>Koko $200</td>
<td>$200</td>
<td>$140</td>
<td>= B6 C6 + $</td>
<td>= D6 B6 / ^ $</td>
</tr>
<tr>
<td>Timezone</td>
<td>$1</td>
<td>$0</td>
<td>= B7 C7 + $</td>
<td>= D7 B7 / ^ $</td>
</tr>
<tr>
<td>Timezone Double Dollars</td>
<td>$1</td>
<td>$1</td>
<td>= B8 C8 + $</td>
<td>= D8 B8 / ^ $</td>
</tr>
</tbody></table>
</div>

<h1 id="the-games">The Games</h1>
<p>The following games have been logged, all tables can be sorted by clicking the header:</p>
<h2 id="beatmania-iidx">Beatmania IIDX</h2>
<p>The cost of Beatmania IIDX at relevant venues:</p>
<h3 id="credits-to-play">Credits to play</h3>
<div class="sortable spreadsheet" data-name="iidx-credits">

<table>
<thead>
<tr>
<th>Venue</th>
<th>Standard</th>
<th>Premium</th>
<th>Credit Cost</th>
</tr>
</thead>
<tbody><tr>
<td>Puri</td>
<td>2</td>
<td>3</td>
<td>$1</td>
</tr>
<tr>
<td>Koko</td>
<td>2</td>
<td>3</td>
<td>$1.45</td>
</tr>
</tbody></table>
</div>

<h3 id="credit-breakdown">Credit Breakdown</h3>
<div class="sortable spreadsheet" data-name="iidx-cost">


<table>
<thead>
<tr>
<th>Venue</th>
<th>Real Credit Cost</th>
<th>$/Standard</th>
<th>$/Premium</th>
</tr>
</thead>
<tbody><tr>
<td>Puri</td>
<td>= !venue-value:E1 !iidx-credits:D1 * ^ $</td>
<td>= !iidx-credits:B1 B1 * ^ $</td>
<td>= !iidx-credits:C1 B1 * ^ $</td>
</tr>
<tr>
<td>Koko</td>
<td>= !venue-value:E2 !iidx-credits:D2 * ^ $</td>
<td>= !iidx-credits:B2 B2 * ^ $</td>
<td>= !iidx-credits:C2 B2 * ^ $</td>
</tr>
<tr>
<td>Koko $30</td>
<td>= !venue-value:E3 !iidx-credits:D2 * ^ $</td>
<td>= !iidx-credits:B2 B3 * ^ $</td>
<td>= !iidx-credits:C2 B3 * ^ $</td>
</tr>
<tr>
<td>Koko $60</td>
<td>= !venue-value:E4 !iidx-credits:D2 * ^ $</td>
<td>= !iidx-credits:B2 B4 * ^ $</td>
<td>= !iidx-credits:C2 B4 * ^ $</td>
</tr>
<tr>
<td>Koko $100</td>
<td>= !venue-value:E5 !iidx-credits:D2 * ^ $</td>
<td>= !iidx-credits:B2 B5 * ^ $</td>
<td>= !iidx-credits:C2 B5 * ^ $</td>
</tr>
<tr>
<td>Koko $200</td>
<td>= !venue-value:E6 !iidx-credits:D2 * ^ $</td>
<td>= !iidx-credits:B2 B6 * ^ $</td>
<td>= !iidx-credits:C2 B6 * ^ $</td>
</tr>
</tbody></table>
</div>

<h2 id="sound-voltex">Sound Voltex</h2>
<p>The cost of Sound Voltex at relevant venues:</p>
<h3 id="credits-to-play">Credits to play</h3>
<div class="sortable spreadsheet" data-name="sdvx-credits">

<table>
<thead>
<tr>
<th>Venue</th>
<th>Standard</th>
<th>Premium</th>
<th>Credit Cost</th>
</tr>
</thead>
<tbody><tr>
<td>Puri</td>
<td>2</td>
<td>3</td>
<td>$1</td>
</tr>
<tr>
<td>Koko</td>
<td>2</td>
<td>3</td>
<td>$1.20</td>
</tr>
<tr>
<td>Timezone</td>
<td>1</td>
<td>2</td>
<td>$2.40</td>
</tr>
</tbody></table>
</div>

<h3 id="credit-breakdown">Credit Breakdown</h3>
<div class="sortable spreadsheet" data-name="sdvx-cost">


<table>
<thead>
<tr>
<th>Venue</th>
<th>Real Credit Cost</th>
<th>$/Standard</th>
<th>$/Premium</th>
</tr>
</thead>
<tbody><tr>
<td>Puri</td>
<td>= !venue-value:E1 !sdvx-credits:D1 * ^ $</td>
<td>= !sdvx-credits:B1 B1 * ^ $</td>
<td>= !sdvx-credits:C1 B1 * ^ $</td>
</tr>
<tr>
<td>Koko</td>
<td>= !venue-value:E2 !sdvx-credits:D2 * ^ $</td>
<td>= !sdvx-credits:B2 B2 * ^ $</td>
<td>= !sdvx-credits:C2 B2 * ^ $</td>
</tr>
<tr>
<td>Koko $30</td>
<td>= !venue-value:E3 !sdvx-credits:D2 * ^ $</td>
<td>= !sdvx-credits:B2 B3 * ^ $</td>
<td>= !sdvx-credits:C2 B3 * ^ $</td>
</tr>
<tr>
<td>Koko $60</td>
<td>= !venue-value:E4 !sdvx-credits:D2 * ^ $</td>
<td>= !sdvx-credits:B2 B4 * ^ $</td>
<td>= !sdvx-credits:C2 B4 * ^ $</td>
</tr>
<tr>
<td>Koko $100</td>
<td>= !venue-value:E5 !sdvx-credits:D2 * ^ $</td>
<td>= !sdvx-credits:B2 B5 * ^ $</td>
<td>= !sdvx-credits:C2 B5 * ^ $</td>
</tr>
<tr>
<td>Koko $200</td>
<td>= !venue-value:E6 !sdvx-credits:D2 * ^ $</td>
<td>= !sdvx-credits:B2 B6 * ^ $</td>
<td>= !sdvx-credits:C2 B6 * ^ $</td>
</tr>
<tr>
<td>Timezone</td>
<td>= !venue-value:E7 !sdvx-credits:D3 * ^ $</td>
<td>= !sdvx-credits:B3 B7 * ^ $</td>
<td>= !sdvx-credits:C3 B7 * ^ $</td>
</tr>
<tr>
<td>Timezone DD</td>
<td>= !venue-value:E8 !sdvx-credits:D3 * ^ $</td>
<td>= !sdvx-credits:B3 B8 * ^ $</td>
<td>= !sdvx-credits:C3 B8 * ^ $</td>
</tr>
</tbody></table>
</div>

<h2 id="gitadora">Gitadora</h2>
<p>The cost of Gitadora at relevant venues</p>
<h3 id="credits-to-play">Credits to play</h3>
<div class="sortable spreadsheet" data-name="dtx-credits">

<table>
<thead>
<tr>
<th>Venue</th>
<th>Light</th>
<th>Standard</th>
<th>Credit Cost</th>
</tr>
</thead>
<tbody><tr>
<td>Timezone</td>
<td>1</td>
<td>2</td>
<td>$2.80</td>
</tr>
</tbody></table>
</div>


<h3 id="credit-breakdown">Credit Breakdown</h3>
<div class="sortable spreadsheet" data-name="dtx-cost">

<table>
<thead>
<tr>
<th>Venue</th>
<th>Real Credit Cost</th>
<th>$/Light</th>
<th>$/Standard</th>
</tr>
</thead>
<tbody><tr>
<td>Timezone</td>
<td>= !venue-value:E7 !dtx-credits:D1 * ^ $</td>
<td>= !dtx-credits:B1 B1 * ^ $</td>
<td>= !dtx-credits:C1 B1 * ^ $</td>
</tr>
<tr>
<td>Timezone DD</td>
<td>= !venue-value:E8 !dtx-credits:D1 * ^ $</td>
<td>= !dtx-credits:B1 B2 * ^ $</td>
<td>= !dtx-credits:C1 B2 * ^ $</td>
</tr>
</tbody></table>
</div>

 ]]></content>
                </entry><entry>
                  <id>https://pfy.ch/games/iidx/for-puri.html</id>
                  <title>My Third Place</title>
                  <published>2025-06-22T08:27:45.132Z</published>
                  <updated>2025-06-22T21:34:02.000Z</updated>
                  <link>https://pfy.ch/games/iidx/for-puri.html</link>
                  <emoji>🎮</emoji>
                  <content type="html" xml:base="https://pfy.ch/games/iidx/for-puri.html"><![CDATA[ <p>Back when I first moved out of home to Sydney I found a really cool venue called Purikura Photo-land, or Puri for short. They&#39;d just gotten a Beatmania IIDX cabinet and two Sound Voltex cabinets from Konami Japan. I&#39;d never heard of or played either game before. But they looked insane.</p>
<p>I stared, watching the people there playing Beatmania for a couple of minutes before one of them, in a really friendly tone, asked if I&#39;d like to play. I&#39;d played a <em>few</em> rhythm games before, but never at the level I just watched them play at. But I said &quot;ok&quot; and they helped navigate me through the menus, set up an account, and configure my settings - All while a dreaded 60-second countdown loomed between each menu!</p>
<p>Once I picked a song I had an absolute blast. It was some of the most fun I&#39;d ever had playing a Rhythm game, and I could see the potential as a game that I could potentially play ... forever.</p>
<p>After I played my credit I asked the twins who&#39;d just helped me if there was any online community and was told that there was a Facebook Chat. I didn&#39;t have Facebook, so we had a quick back and forth to try and find some shared social platform, so we could at least add each other and stay in touch. We ended up all having WeChat accounts, which they found funny - &quot;The white girl with WeChat&quot; was how they described me to the guys in the Facebook group at the time which I also found funny.</p>
<p><img src="https://assets.pfy.ch/md/DSCF0594.jpg" alt="" /></p>
<p>Coming to Puri became a routine for me, I eventually made a Facebook account just for the chat and connected with heaps of really cool people. On weekends, at its peak, there would be at least ten to fifteen of us all hanging around, waiting to play (and sometimes we&#39;d talk!).</p>
<p>Through the people here, I got into the wider Sydney arcade scene, which had really only just started growing as both Sega &amp; Konami had expanded their online services to Australia.</p>
<p>It was a great community, and without Puri, I never would have met all the amazing people I now call some of my closest friends or even my partner. It was a place we could all easily meet up, go for dinner and hang out. And without this community, I don&#39;t know where I&#39;d be, or who I&#39;d be right now. It was the first group of people since moving out of home I could really call close friends.</p>
<p><img src="https://assets.pfy.ch/md/A001660-R1-04-5.jpg" alt="I had the cheapest film loaded in my camera at the time (and my settings were wrong)" /></p>
<p>Over the next 5+ years, the foot traffic dwindled. Newer, shinier arcades opened around the city pulling more people. They had convenient cards<sup><a id="footnote-ref-1" href="#footnote-1" data-footnote-ref aria-describedby="footnote-label">1</a></sup>  you could load with money, so you didn&#39;t need to carry coins. And they had more variety.</p>
<p>I&#39;d still go to the new venues to hang out with people, play new games, and just chill. But I&#39;d always end up coming back to Puri. They were a quieter, cheaper, and safer-feeling venue. The big open atrium above the cabs and the low foot traffic made the place feel ... homely. It was always a constant, and a safe place I could fall back on that wasn&#39;t my shitty apartment or office at work.</p>
<p><img src="https://assets.pfy.ch/md/CRO2648-R1-33-4A.jpg" alt="" /></p>
<p>Sadly, as of the 21st of June 2025, Puri staff have quietly let us know that they&#39;re selling all their Rhythm game cabs. There&#39;s no known date when they&#39;ll be sold, or when their last playable days are. But they&#39;ve said they&#39;d let us know when it is, so we can all get one last game in.<sup><a id="footnote-ref-2" href="#footnote-2" data-footnote-ref aria-describedby="footnote-label">2</a></sup></p>
<p>It felt weird getting emotional over a venue selling machines. I genuinely got really upset, like losing a close friend.</p>
<p>Reflecting, it felt weird to hold a <em>place</em> in that high of regard. But it had been a constant in my life, every weekend, every tough day after work, and whenever I was down in the shits. I knew I had a place where I could rock up with change, hang out with friends and kill a couple hours. There are other venues, yes, but the rhythm game setup at Puri meant a lot to me. Even after a majority of the community went elsewhere.</p>
<p>It&#39;s not really anyone&#39;s fault that Puri is selling the machines, it makes sense as a business. They take up valuable floor space, and they probably don&#39;t pull in much money anymore. But that doesn&#39;t make it any less sad.</p>
<p><img src="https://assets.pfy.ch/md/A001660-R1-06-7.jpg" alt="" /></p>
<p>I genuinely don&#39;t know where my next &quot;place&quot; will be. All the other arcades are crowded, cramped and noisy<sup><a id="footnote-ref-3" href="#footnote-3" data-footnote-ref aria-describedby="footnote-label">3</a></sup>. I&#39;ll probably still go to get a few credits into certain games and hang out with friends. One of them just got Gitadora Drums, which I&#39;ve been waiting for somewhere to get for years. But with the only other location that has a Beatmania IIDX cabinet charging $7 for a premium credit, when Puri charged $3, I definitely see myself playing <a class=""  href="../bms.html">BMS</a> at home a lot more than before.</p>
<p>I&#39;ll miss Puri. A lot.</p>
<p><img src="https://assets.pfy.ch/lg/CRO2648-R1-32-5A.jpg" alt="I took this photo of a score with a film Camera!" /></p>
<p><img src="https://assets.pfy.ch/lg/CRO2648-R1-35-2A.jpg" alt="My (now) partner who got into the game to spend more time with me :3c" /></p>
<p><img src="https://assets.pfy.ch/md/CRO2648-R1-31-6A.jpg" alt="A friend in front of the whiteboard we used to track turns (and scribble on)" /></p>
<p><img src="https://assets.pfy.ch/md/A001660-R1-05-6.jpg" alt="" /></p>
<p><bsky-comments data-post-id="3ls7zgiubuc23"></bsky-comments></p>
<section class="footnotes" data-footnotes>
<h2 id="footnote-label" class="sr-only">Footnotes</h2>
<ol>
<li id="footnote-1">
<p>This also means the games cost more per play! It&#39;s cheaper if you take advantage of top-up matching or double top-up deals. However, this is really only true if you add <em>extremely</em> high amounts to your card. As Inconvenient as cash is, I can use it everywhere. If I put $600 on my Arcade card to &quot;take advantage of a deal&quot;, I&#39;ve now got $1200 of Arcade-Bucks I can&#39;t spend anywhere except one venue. It sucks! <a href="#footnote-ref-1" data-footnote-backref aria-label="Back to reference 1">↩</a></p>
</li>
<li id="footnote-2">
<p><strong>Update:</strong> The last day is likely the 29th of June, I&#39;ll be out of state that weekend, so I&#39;ll be unable to attend the organized &quot;last day&quot; meetup. Which is really a shame. <a href="#footnote-ref-2" data-footnote-backref aria-label="Back to reference 2">↩</a></p>
</li>
<li id="footnote-3">
<p>Earplugs are <strong>mandatory</strong> if spending a lot of time in these venues, the volume averages around 90db! <a href="#footnote-ref-3" data-footnote-backref aria-label="Back to reference 3">↩</a></p>
</li>
</ol>
</section>
 ]]></content>
                </entry><entry>
                  <id>https://pfy.ch/programming/projects/bms-torrent.html</id>
                  <title>BMS Torrent</title>
                  <published>2025-06-20T01:47:59.276Z</published>
                  <updated>2025-06-20T01:55:57.000Z</updated>
                  <link>https://pfy.ch/programming/projects/bms-torrent.html</link>
                  <emoji>💻</emoji>
                  <content type="html" xml:base="https://pfy.ch/programming/projects/bms-torrent.html"><![CDATA[ <ul>
<li><a class=""  href="#goals">Goals</a><ul>
<li><a class=""  href="#what-about-the-big-sister-bms-project-bsbp">What about the Big Sister BMS Project?</a></li>
</ul>
</li>
<li><a class=""  href="#benefits">Benefits</a></li>
<li><a class=""  href="#methodology">Methodology</a><ul>
<li><a class=""  href="#folder-structure">Folder Structure</a></li>
<li><a class=""  href="#file-formats">File formats</a></li>
</ul>
</li>
<li><a class=""  href="#the-index">The Index</a></li>
</ul>
<h1 id="goals">Goals</h1>
<p>The primary goal of this project is to offer a torrent of <em>uncompressed</em> BMS charts, allowing for partial downloads &amp; easy contribution by seeding existing BMS folders. A secondary goal of this project is to offer a hosted index of this torrent mapping <code>md5</code> &amp; <code>sha256</code> to the path of the chart in the torrent, allowing for quick discovery, partial downloads and potential future integrations with clients.</p>
<h2 id="what-about-the-big-sister-bms-project-bsbp">What about the Big Sister BMS Project (BSBP)?</h2>
<p>The BSBP is an amazing resource! However, the bandwidth and storage costs are a potential burden, and relying on donations to cover costs <em>may</em> be unsustainable in the long term.</p>
<p>A generalised <em>public torrent</em>, uncompressed, allows for longer term preservation of BMS charts, allowing for users to seed directly from their active BMS folder (Assuming they follow a flat directory structure).</p>
<p>This project is not a replacement, or competitor, to the BSBP!</p>
<h1 id="benefits">Benefits</h1>
<p>By distributing charts via BitTorrent, users can quickly and easily pull specific charts &amp; updates in a distributed manner without relying on a 3rd party to host and bear the cost for serving these files.</p>
<p>New versions of the core <code>.torrent</code> file can be distributed which, through most torrent clients, will only download changed &amp;/ new files. Allowing for quick library updates.</p>
<h1 id="methodology">Methodology</h1>
<p>This projects core will consist of a portable &amp; easily runnable script which generates both the torrent and an index given a path.</p>
<p>This script will ideally be run on an existing folder containing all charts in popular and common tables.</p>
<pre><code class="hljs language-bash">node generate-torrent.js -- /path/to/folder <span class="hljs-comment"># Language not set in stone!</span>
<span class="hljs-built_in">ls</span> /out
bms-2025-01-01.torrent    bms-2025-01-01.json 
</code></pre><p>This torrent can then be distributed by the user who generated it.</p>
<p>Ideally an initial centralised torrent will be created and distributed alongside the index, This simplifies onboarding of new players and ensures a high seed-count for users when downloading. A centralised distributer also allows easier updates to the core torrent when a new version is released with notifications provided to seeders via a chat service<sup><a id="footnote-ref-1" href="#footnote-1" data-footnote-ref aria-describedby="footnote-label">1</a></sup>.</p>
<p>Torrents are tagged with their creation date to facilitate easier discussions with people wanting to update &amp;/ check if they are using the latest version.</p>
<blockquote>
<p><strong>Foo</strong>: &quot;Hey whats the latest torrent&quot;<br><strong>Bar</strong>: &quot;2025-01-01 is the latest right now&quot;<br><strong>Foo:</strong> &quot;Ah I&#39;m behind, can you send the file or magnet link through?&quot;<br><strong>Bar:</strong> &quot;Here you go: &lt;attached file&gt;&quot;</p>
</blockquote>
<p>In theory users <em><strong>could</strong></em> seed multiple versions of the torrent. However, there is a small chance of conflicting files if <em>content</em> of a distributed <code>.bms</code> file is changed, without changing the filename.</p>
<h2 id="folder-structure">Folder Structure</h2>
<p>Folder structure may be a contentious topic, with some users potentially wanting to merge the contents of the torrent with their existing filesystem &amp; collection of BMS.</p>
<p>A flat hierarchy makes the most sense for simplicity:
<code>/root/&lt;chart-name&gt;/&lt;chart&gt;.&lt;extension&gt;</code></p>
<p>However some people may organise their collections in different ways. This is worth investigating to discover if there are any better ways to organise the torrent.</p>
<h2 id="file-formats">File formats</h2>
<p>Some charts are distributed wth multiple versions. Some with and without videos, <code>.wav</code> &amp; <code>.mp3</code>, etc. </p>
<p>I believe this torrent should distribute &quot;the most correct&quot; version of the chart, which may be a divisive topic. This means that the if a no video version is offered alongside a video version, this torrent will distribute the version <em>with</em> the video.</p>
<p>Regarding audio formats, it&#39;s worth investigating and discussing with players. The format <code>.wav</code> makes sense, however some charts are distributed &quot;originally&quot; with massive uncompressed <code>.flac</code> sounds. This may however, be a non-issue in practice.</p>
<h1 id="the-index">The Index</h1>
<p>Assume the torrent is laid out in the following way:</p>
<pre><code class="hljs">bms
└ chart-name
  ├ chart-normal.bms
  ├ chart-hyper.bms
  ├ chart-another.bms
  ├ hitsound.wav
  └ video.mp4
</code></pre><p>Provide a JSON file &amp;/ HTTP endpoint to convert the <code>sha</code> of <code>chart-normal.bms</code> to a path:</p>
<pre><code class="hljs language-http"><span class="hljs-keyword">GET</span> <span class="hljs-string">/path/bf6bcb1370fe4956ceb5ea436bdb63bb</span> <span class="hljs-meta">HTTP/1.1</span>
<span class="hljs-attribute">content-type</span><span class="hljs-punctuation">: </span>application/json

<span class="language-css">{
	&quot;<span class="hljs-selector-tag">path</span>&quot;: <span class="hljs-string">&quot;/bms/chart-name&quot;</span>
}</span>
</code></pre><pre><code class="hljs language-json"><span class="hljs-punctuation">{</span>
	<span class="hljs-attr">&quot;bf6bcb1370fe4956ceb5ea436bdb63bb&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;/bms/chart-name&quot;</span><span class="hljs-punctuation">,</span>
	<span class="hljs-attr">&quot;f4d1d4c7bbfa6007835aaab6bfe11435&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;/bms/chart-name&quot;</span><span class="hljs-punctuation">,</span>
	<span class="hljs-attr">&quot;911dc9e1f0549ca12990892e113d255b&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;/bms/chart-name&quot;</span>
<span class="hljs-punctuation">}</span>
</code></pre><p>A standardised index file can allow for a potential future integration with popular clients, allowing them to integrate with either an external (or internal) bitTorrent client. Allowing charts to be downloaded on the fly.</p>
<section class="footnotes" data-footnotes>
<h2 id="footnote-label" class="sr-only">Footnotes</h2>
<ol>
<li id="footnote-1">
<p>A custom tracker would potentially allow notifying via the torrent client, however this is potentially out of scope for this initial simple project. RSS Feeds, Discord announcements, Word of mouth, can facilitate update notifications for now. <a href="#footnote-ref-1" data-footnote-backref aria-label="Back to reference 1">↩</a></p>
</li>
</ol>
</section>
 ]]></content>
                </entry><entry>
                  <id>https://pfy.ch/programming/projects/mangadex-chapters.html</id>
                  <title>MangaDex Chapter checker</title>
                  <published>2025-04-20T05:34:23.758Z</published>
                  <updated>2025-04-20T06:01:19.000Z</updated>
                  <link>https://pfy.ch/programming/projects/mangadex-chapters.html</link>
                  <emoji>💻</emoji>
                  <content type="html" xml:base="https://pfy.ch/programming/projects/mangadex-chapters.html"><![CDATA[ <p>I&#39;ve run a dumb cron-job shell script for a while now that Pulls the chapter list page from MangaDex for some set manga and pings me on Discord if any new chapters are released. It was a bit janky, but it really did the job for the year or so I ran it.</p>
<p>However, recently I had a friend reach out to me to see if I could extend it to <em>also</em> ping them if chapters were updated for a specific manga &amp; my shell script definitely could not do that without a major refactor, so I used it as an excuse to rewrite it and learn some new stuff along the way.</p>
<p><a class="externalLink" target="_blank" href="https://git.pfy.ch/pfych/chapter-tracker">The code is available on my git host if you'd like to check it out!</a></p>
<h2 id="building-docker-images">Building Docker Images</h2>
<p>This is the first time I&#39;ve ever built a project from scratch and wanted to have it run somewhere other than the cloud - this is a simple lightweight node script with no uptime requirements, so it&#39;s a perfect candidate to use to learn about containerising stuff!</p>
<p>I&#39;ve used docker for years now running tools, programs and scripts other people have written, but I&#39;ve never actually dockerized anything on my own since I thought it would be really daunting and difficult! In reality - making the actual image was <em>really simple!</em></p>
<pre><code class="hljs language-dockerfile"><span class="hljs-keyword">FROM</span> node:<span class="hljs-number">20</span>-alpine

<span class="hljs-keyword">WORKDIR</span><span class="language-bash"> /checker</span>

<span class="hljs-keyword">COPY</span><span class="language-bash"> package.json ./</span>
<span class="hljs-keyword">COPY</span><span class="language-bash"> pnpm-lock.yaml ./</span>
<span class="hljs-keyword">COPY</span><span class="language-bash"> src ./src</span>

<span class="hljs-keyword">LABEL</span><span class="language-bash"> org.opencontainers.image.source=https://git.pfy.ch/pfych/chapter-tracker</span>
<span class="hljs-keyword">LABEL</span><span class="language-bash"> org.opencontainers.image.description=<span class="hljs-string">&quot;Ping a webhook when a new MangaDex chapter is released&quot;</span></span>
<span class="hljs-keyword">LABEL</span><span class="language-bash"> org.opencontainers.image.licenses=MIT</span>

<span class="hljs-keyword">RUN</span><span class="language-bash"> npm install -g pnpm</span>
<span class="hljs-keyword">RUN</span><span class="language-bash"> pnpm install</span>
<span class="hljs-keyword">RUN</span><span class="language-bash"> pnpm run compile</span>

<span class="hljs-keyword">CMD</span><span class="language-bash"> [<span class="hljs-string">&quot;node&quot;</span>, <span class="hljs-string">&quot;./.out/build.js&quot;</span>]</span>
</code></pre><p>The hard part came from the question &quot;Now how do I run this on my NAS?&quot;.</p>
<p>Turns out Gitea (which is what I use to host my repos &amp; runners) has a <a class="externalLink" target="_blank" href="https://docs.gitea.com/usage/packages/container">Container Registry</a> component that allows you to publish images alongside a repo! I won&#39;t go into any of the <a class="externalLink" target="_blank" href="https://git.pfy.ch/pfych/chapter-tracker/src/branch/main/.gitea/workflows/build-container.yml">actions I used to automatically build and publish the image</a>, but it really was as simple as a few commands to publish a package to my gitea instance.</p>
<pre><code class="hljs language-bash">docker login git.pfy.ch
docker build -t git.pfy.ch/pfych/chapter-tracker:latest
docker push git.pfy.ch/pfych/chapter-tracker:latest
</code></pre><p>So easy!</p>
<p>And now I can just reference that from my compose file!</p>
<pre><code class="hljs language-yaml"><span class="hljs-attr">services:</span>
  <span class="hljs-attr">checker:</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">git.pfy.ch/pfych/chapter-tracker:latest</span>
    <span class="hljs-attr">volumes:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">&quot;./config.json:/checker/config.json&quot;</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">&quot;./mangaHistory.json:/checker/mangaHistory.json&quot;</span> 
</code></pre><p>I&#39;m definitely going to be more likely to write non-cloud scripts now that I&#39;ve actually <em>tried</em> and succeeded in dockerizing a Node project. I&#39;m really surprised it&#39;s taken me this long.</p>
<p><bsky-comments data-post-id="3ln7xwdhswk2p"></bsky-comments></p>
 ]]></content>
                </entry><entry>
                  <id>https://pfy.ch/programming/the-images-page.html</id>
                  <title>The Images Page</title>
                  <published>2025-03-27T05:39:28.743Z</published>
                  <updated>2025-03-27T05:45:12.000Z</updated>
                  <link>https://pfy.ch/programming/the-images-page.html</link>
                  <emoji>💻</emoji>
                  <content type="html" xml:base="https://pfy.ch/programming/the-images-page.html"><![CDATA[ <p>the <a class=""  href="/images.html">images</a> page on my site lists every single image I&#39;ve ever published on my website. I&#39;m actually quite happy with how it all works, and I realised I&#39;ve never broken it down before!</p>
<p>Before uploading any images to my site, I need to compress them. Images out of my camera are <em>huge</em> and I don&#39;t want people downloading 25mb+ images on random pages of my site. I use ImageMagick and a <a class="externalLink" target="_blank" href="https://git.pfy.ch/pfych/unix-config/src/branch/main/.config/scripts/tool/site-prep.sh">little helper function I wrote</a> to do this.</p>
<p>I generate a <code>sm</code>, <code>md</code> and <code>lg</code> version of each image with the following commands:</p>
<pre><code class="hljs language-bash"><span class="hljs-comment"># A thumbnail version of the image</span>
convert originalImage.jpg -strip -interlace Plane -scale 10% -blur 0x2.5 -resize 1000% -resize 180 <span class="hljs-string">&quot;sm/<span class="hljs-variable">${1%.*}</span>.jpg&quot;</span>

<span class="hljs-comment"># A suprisingly good quality version of the image</span>
convert originalImage.jpg -strip -interlace Plane -gaussian-blur 0.05 -quality 85% -resize 720 <span class="hljs-string">&quot;md/<span class="hljs-variable">${1%.*}</span>.jpg&quot;</span>

<span class="hljs-comment"># A nicer &quot;HD&quot; version of the image</span>
convert originalImage.jpg -strip -quality 85% -interlace Plane -resize 1800 <span class="hljs-string">&quot;lg/<span class="hljs-variable">${1%.*}</span>.jpg&quot;</span>
</code></pre><p>Once I&#39;ve generated all the images, they&#39;re added to my sites storage in folders. My site generator then reads all the files in the <code>sm</code> folder, and generates a html for all the images. The html for each image looks something like this:</p>
<pre><code class="hljs language-html"><span class="hljs-tag">&lt;<span class="hljs-name">img</span> 
  <span class="hljs-attr">src</span>=<span class="hljs-string">&quot;https://assets.pfy.ch/sm/image.jpg&quot;</span> 
  <span class="hljs-attr">finalSrc</span>=<span class="hljs-string">&quot;https://assets.pfy.ch/md/image.jpg&quot;</span> 
  <span class="hljs-attr">hdSrc</span>=<span class="hljs-string">&quot;https://assets.pfy.ch/lg/image.jpg&quot;</span> 
/&gt;</span>
</code></pre><p>By default the <code>sm</code> version of the image will load, when the images is in view, I swap out the <code>src</code> with the value in the <code>finalSrc</code> attribute. This is done using an <a class="externalLink" target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API">Intersection Observer</a>.</p>
<pre><code class="hljs language-ts"><span class="hljs-keyword">const</span> images = <span class="hljs-variable language_">document</span>.<span class="hljs-title function_">querySelectorAll</span>(<span class="hljs-string">&#x27;.galleryImage&#x27;</span>);

<span class="hljs-keyword">const</span> <span class="hljs-title function_">loadImage</span> = (<span class="hljs-params"><span class="hljs-attr">image</span>: <span class="hljs-title class_">HTMLImageElement</span></span>) =&gt; {
  image.<span class="hljs-property">src</span> = image.<span class="hljs-title function_">getAttribute</span>(<span class="hljs-string">&#x27;finalSrc&#x27;</span>) || <span class="hljs-string">&#x27;&#x27;</span>;
};

<span class="hljs-keyword">const</span> <span class="hljs-title function_">handleIntersection</span> = (<span class="hljs-params"><span class="hljs-attr">entries</span>: <span class="hljs-title class_">IntersectionObserverEntry</span>[]</span>) =&gt; {
  entries.<span class="hljs-title function_">forEach</span>(<span class="hljs-function">(<span class="hljs-params">entry</span>) =&gt;</span> {
    <span class="hljs-keyword">if</span> (entry.<span class="hljs-property">intersectionRatio</span> &gt; <span class="hljs-number">0</span>) {
      <span class="hljs-title function_">loadImage</span>(entry.<span class="hljs-property">target</span> <span class="hljs-keyword">as</span> <span class="hljs-title class_">HTMLImageElement</span>);
    }
  });
};

<span class="hljs-keyword">const</span> observer = <span class="hljs-keyword">new</span> <span class="hljs-title class_">IntersectionObserver</span>(handleIntersection, {
  <span class="hljs-attr">root</span>: <span class="hljs-literal">null</span>,
  <span class="hljs-attr">rootMargin</span>: <span class="hljs-string">&#x27;0px&#x27;</span>,
  <span class="hljs-attr">threshold</span>: <span class="hljs-number">0.05</span>,
});

images.<span class="hljs-title function_">forEach</span>(<span class="hljs-function">(<span class="hljs-params">img</span>) =&gt;</span> {
  observer.<span class="hljs-title function_">observe</span>(img);

  img.<span class="hljs-title function_">addEventListener</span>(<span class="hljs-string">&#x27;click&#x27;</span>, <span class="hljs-function">(<span class="hljs-params">event</span>) =&gt;</span> {
    <span class="hljs-keyword">const</span> image = event.<span class="hljs-property">target</span> <span class="hljs-keyword">as</span> <span class="hljs-title class_">HTMLImageElement</span>;

    image.<span class="hljs-property">onerror</span> = <span class="hljs-function">() =&gt;</span> {
      <span class="hljs-variable language_">console</span>.<span class="hljs-title function_">warn</span>(<span class="hljs-string">&#x27;Could not get LG version of image!&#x27;</span>);
      image.<span class="hljs-property">src</span> = image.<span class="hljs-title function_">getAttribute</span>(<span class="hljs-string">&#x27;finalSrc&#x27;</span>) || <span class="hljs-string">&#x27;&#x27;</span>;
    };

    image.<span class="hljs-property">src</span> = image.<span class="hljs-title function_">getAttribute</span>(<span class="hljs-string">&#x27;hdSrc&#x27;</span>) || <span class="hljs-string">&#x27;&#x27;</span>;
  });
});
</code></pre><p>I would love to have the images popup or go fullscreen when they&#39;re clicked, but I didn&#39;t want to make it too complex. </p>
<p>Since they&#39;re just plain HTML images, you can just open them in a new tab or window. Which seems to work fine for most people who browse my photos!</p>
 ]]></content>
                </entry><entry>
                  <id>https://pfy.ch/programming/webmentions.html</id>
                  <title>Re-enabling webmentions</title>
                  <published>2025-03-23T00:13:17.474Z</published>
                  <updated>2025-03-23T00:36:12.000Z</updated>
                  <link>https://pfy.ch/programming/webmentions.html</link>
                  <emoji>💻</emoji>
                  <content type="html" xml:base="https://pfy.ch/programming/webmentions.html"><![CDATA[ <p>A while back I wrote about creating a dedicated serverless <a class=""  href="projects/webmentions.html">webmention receiver</a> for my site. It was pretty good and super reliable, but it existed in its own repo, used a different AWS toolkit &amp; was separate from the rest of my site and backend. This lead to it sort of being abandoned and not maintained. Luckily, it was written quite robustly and still worked, but I was unhappy with it living separate to my existing backends CI/CD process, testing &amp; standard toolset. </p>
<p>If you haven&#39;t read the previous post, webmentions are a standard way of pinging a site to let them know that you&#39;ve linked to them. My site has the following tag in it&#39;s <code>&lt;head&gt;</code>:</p>
<pre><code class="hljs language-html"><span class="hljs-tag">&lt;<span class="hljs-name">link</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;externalLink&quot;</span> <span class="hljs-attr">target</span>=<span class="hljs-string">&quot;_blank&quot;</span> <span class="hljs-attr">href</span>=<span class="hljs-string">&quot;https://api.pfy.ch/webmention&quot;</span> <span class="hljs-attr">rel</span>=<span class="hljs-string">&quot;webmention&quot;</span>&gt;</span>
</code></pre><p>Posting to this URL with a JSON payload containing a <code>source</code> (somewhere on your site) and a <code>destination</code> (somewhere on my site) will add a little backlink to your post on my webpage acknowledging that it&#39;s been referred to elsewhere on the web! Obviously there are check&#39;s in place, but it really is that simple. As long as the <code>source</code> contains a direct link to <code>destination</code> it gets through!</p>
<p>Just like with my previous implementation - I <em>still</em> haven&#39;t gotten around to actually pinging other peoples webmention endpoints from my custom CMS... It shouldn&#39;t be too difficult, I&#39;m just a bit lazy and weary of pinging people <em>too much</em>.</p>
 ]]></content>
                </entry><entry>
                  <id>https://pfy.ch/programming/jsx-clientside.html</id>
                  <title>Using JSX for a better developer experience</title>
                  <published>2025-03-17T02:47:15.353Z</published>
                  <updated>2025-03-17T02:50:32.000Z</updated>
                  <link>https://pfy.ch/programming/jsx-clientside.html</link>
                  <emoji>💻</emoji>
                  <content type="html" xml:base="https://pfy.ch/programming/jsx-clientside.html"><![CDATA[ <p>99% of this website is statically generated HTML using my SSG which I&#39;ve been modifying and maintaining since 2019. What started as a Bash script, has grown immensely since first starting and the final output has not really changed too much. However, sometimes, I like to put up cool interactive pages on my site like my <a class=""  href="../music/music.html">Music page</a>, <a class=""  href="../games/games.html">Games page</a>, or even the <a class=""  href="../games/mahjong/scoring-calculator.html">Mahjong scoring page</a>.</p>
<p>However, there are only so many times you can use <code>document.createElement();</code> before you go insane.</p>
<h2 id="how-scripts-work-on-my-site">How scripts work on my site</h2>
<p>I write all my scripts in Typescript. Using Esbuild, I bundle these scripts into individual minified <code>js</code> files. My custom SSG tool handles hot reloads and can do all this on the fly, so there&#39;s no real friction in using Typescript over plain Javascript.</p>
<p>Every page on my site is defined as a Markdown file, using the front matter I can set the title, summary, publish date etc. but also include a list of scripts. By doing this, over a central <code>script.js</code> file (although I do have one), I can keep load times for my site low since pages will only load the scripts essential for them to function.</p>
<h2 id="how-i-initially-did-interactive-pages">How I initially did interactive pages</h2>
<p>When I first was adding JS &amp; Interactive pages to my site, pages would look something like this:</p>
<pre><code class="hljs language-markdown">---
title: example
<span class="hljs-section">scripts: old/example.ts
---</span>

Hello World

<span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;example&quot;</span>&gt;</span></span>

<span class="language-xml"><span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
</code></pre><p>and the following script:</p>
<pre><code class="hljs language-ts"><span class="hljs-variable language_">window</span>.<span class="hljs-title function_">addEventListener</span>(<span class="hljs-string">&#x27;DOMContentLoaded&#x27;</span>, <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">const</span> <span class="hljs-attr">target</span>: <span class="hljs-title class_">HTMLDivElement</span> | <span class="hljs-literal">null</span> = [
    ...(<span class="hljs-variable language_">document</span>.<span class="hljs-title function_">getElementsByClassName</span>(<span class="hljs-string">&quot;example&quot;</span>) || [])
  ]?.[<span class="hljs-number">0</span>]
  
  <span class="hljs-keyword">if</span> (target) {
    <span class="hljs-keyword">const</span> p = <span class="hljs-variable language_">document</span>.<span class="hljs-title function_">createElement</span>(<span class="hljs-string">&quot;p&quot;</span>);
    p.<span class="hljs-property">innerText</span> = <span class="hljs-string">&quot;Hello World&quot;</span>
    
    target.<span class="hljs-title function_">append</span>(p)
  }
})
</code></pre><p>While this worked, the developer experience was quite tedious. Especially with larger components. I eventually wrote this function to somewhat assist with element creation:</p>
<pre><code class="hljs language-ts"><span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> e = &lt;T <span class="hljs-keyword">extends</span> keyof <span class="hljs-title class_">HTMLElementTagNameMap</span>&gt;<span class="hljs-function">(<span class="hljs-params">
  <span class="hljs-attr">tag</span>: T,
  <span class="hljs-attr">attributes</span>: <span class="hljs-title class_">Attributes</span>&lt;T&gt; | <span class="hljs-literal">null</span>,
  <span class="hljs-attr">children</span>: (<span class="hljs-built_in">string</span> | HTMLElement)[] | <span class="hljs-built_in">string</span> | <span class="hljs-title class_">HTMLElement</span> = [],
</span>) =&gt;</span> {
  <span class="hljs-keyword">const</span> element = <span class="hljs-variable language_">document</span>.<span class="hljs-title function_">createElement</span>(tag);
  <span class="hljs-title class_">Object</span>.<span class="hljs-title function_">entries</span>(attributes || {}).<span class="hljs-title function_">forEach</span>(<span class="hljs-function">(<span class="hljs-params">[key, value]</span>) =&gt;</span> {
    <span class="hljs-keyword">const</span> castKey = key <span class="hljs-keyword">as</span> keyof <span class="hljs-title class_">HTMLElementTagNameMap</span>[T];
    element[castKey] = value;
  });

  <span class="hljs-keyword">if</span> (<span class="hljs-title class_">Array</span>.<span class="hljs-title function_">isArray</span>(children)) {
    children.<span class="hljs-title function_">forEach</span>(<span class="hljs-function">(<span class="hljs-params">child</span>) =&gt;</span> {
      element.<span class="hljs-title function_">append</span>(child);
    });
  } <span class="hljs-keyword">else</span> {
    element.<span class="hljs-title function_">append</span>(children);
  }

  <span class="hljs-keyword">return</span> element;
};
</code></pre><p>Which turned the previous code into something like this:</p>
<pre><code class="hljs language-ts"><span class="hljs-keyword">import</span> {e} <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;../element&#x27;</span>;

<span class="hljs-variable language_">window</span>.<span class="hljs-title function_">addEventListener</span>(<span class="hljs-string">&#x27;DOMContentLoaded&#x27;</span>, <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-comment">// fetch target ... </span>
  
  <span class="hljs-keyword">if</span> (target) {
    target.<span class="hljs-title function_">append</span>(<span class="hljs-title function_">e</span>(<span class="hljs-string">&#x27;p&#x27;</span>, {}, <span class="hljs-string">&quot;Hello World&quot;</span>))
  }
})
</code></pre><p>This was great, and a big leap in dev QOL. But I really, really, really, wanted to use something as slick as JSX. Turns out - It&#39;s mostly just a tweak in my ts config. I could even re-use the utility function I had written before!</p>
<h2 id="using-tsx">Using TSX</h2>
<p>By setting the <code>jsxFactory</code> property in my config to the function <code>e</code>, when typescript/esbuild compiles a file - instead of inserting a React specific <code>React.createElement()</code>, it&#39;ll insert my function instead.</p>
<pre><code class="hljs language-json"><span class="hljs-punctuation">{</span>
  <span class="hljs-attr">&quot;compilerOptions&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
    <span class="hljs-attr">&quot;jsx&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;react&quot;</span><span class="hljs-punctuation">,</span>
    <span class="hljs-attr">&quot;jsxFactory&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;jsx&quot;</span><span class="hljs-punctuation">,</span>
  <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
<span class="hljs-punctuation">}</span>
</code></pre><p>Without setting <code>jsx</code> to <code>react</code> pretty much every linter will complain when using TSX. I also needed to create a type definition in my project root, otherwise I wouldn&#39;t get any typing. You technically get this for free if I installed React as a dependency but that&#39;s overkill.</p>
<pre><code class="hljs language-ts"><span class="hljs-comment">/// &lt;reference lib=&quot;DOM&quot; /&gt;</span>

<span class="hljs-keyword">declare</span> <span class="hljs-keyword">namespace</span> <span class="hljs-title class_">JSX</span> {
  <span class="hljs-keyword">type</span> <span class="hljs-title class_">Element</span> = <span class="hljs-title class_">HTMLElement</span>;

  <span class="hljs-keyword">interface</span> <span class="hljs-title class_">IntrinsicElements</span> <span class="hljs-keyword">extends</span> <span class="hljs-title class_">IntrinsicElementMap</span> {}

  <span class="hljs-keyword">type</span> <span class="hljs-title class_">IntrinsicElementMap</span> = {
    [K <span class="hljs-keyword">in</span> keyof <span class="hljs-title class_">HTMLElementTagNameMap</span>]: {
      [<span class="hljs-attr">k</span>: <span class="hljs-built_in">string</span>]: <span class="hljs-built_in">any</span>;
    };
  };

  <span class="hljs-keyword">interface</span> <span class="hljs-title class_">Component</span> {
    (<span class="hljs-attr">properties</span>?: { [<span class="hljs-attr">key</span>: <span class="hljs-built_in">string</span>]: <span class="hljs-built_in">any</span> }, <span class="hljs-attr">children</span>?: <span class="hljs-title class_">Node</span>[]): <span class="hljs-title class_">Node</span>;
  }
}
</code></pre><p>I ended up renaming <code>e</code> to <code>JSX</code> for easier importing and now the above script looks like this:</p>
<pre><code class="hljs language-ts"><span class="hljs-keyword">import</span> <span class="hljs-variable constant_">JSX</span> <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;../jsx&#x27;</span> 

<span class="hljs-variable language_">window</span>.<span class="hljs-title function_">addEventListener</span>(<span class="hljs-string">&#x27;DOMContentLoaded&#x27;</span>, <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-comment">// fetch target ... </span>
  
  <span class="hljs-keyword">if</span> (target) {
    target.<span class="hljs-title function_">append</span>(<span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>Hello World<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span></span>)
  }
})
</code></pre><p>I genuinely don&#39;t know why I didn&#39;t do this sooner - using JSX for the interactive parts of my site is so much nicer than what I was doing before.</p>
<pre><code class="hljs language-ts"><span class="hljs-keyword">import</span> <span class="hljs-variable constant_">JSX</span> <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;../jsx&#x27;</span>;

<span class="hljs-keyword">const</span> postsList = (
  <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">ul</span> <span class="hljs-attr">className</span>=<span class="hljs-string">&quot;postsList&quot;</span>&gt;</span>
    {...posts.map((post) =&gt; (
      <span class="hljs-tag">&lt;<span class="hljs-name">li</span>&gt;</span>
        {post.emoji || &#x27;📝&#x27;}
        <span class="hljs-tag">&lt;<span class="hljs-name">a</span> <span class="hljs-attr">href</span>=<span class="hljs-string">{post.link.replace(</span>&#x27;<span class="hljs-attr">https:</span>//<span class="hljs-attr">pfy.ch</span>&#x27;, &#x27;&#x27;)}&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">span</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">className</span>=<span class="hljs-string">&quot;date&quot;</span>&gt;</span>
              {post.date.toLocaleDateString()} -{&#x27; &#x27;}
            <span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
            {post.title.replaceAll(&#x27;<span class="hljs-symbol">&amp;amp;</span>&#x27;, &#x27;&amp;&#x27;)}
          <span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">li</span>&gt;</span>
    ))}
  <span class="hljs-tag">&lt;/<span class="hljs-name">ul</span>&gt;</span></span>
);
</code></pre><p>vs</p>
<pre><code class="hljs language-ts"><span class="hljs-keyword">import</span> { e } <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;../element&#x27;</span>;

<span class="hljs-keyword">const</span> postsList = <span class="hljs-title function_">e</span>(
  <span class="hljs-string">&#x27;ul&#x27;</span>,
  { <span class="hljs-attr">className</span>: <span class="hljs-string">&#x27;postsList&#x27;</span> },
  posts.<span class="hljs-title function_">map</span>(<span class="hljs-function">(<span class="hljs-params">post</span>) =&gt;</span>
    <span class="hljs-title function_">e</span>(<span class="hljs-string">&#x27;li&#x27;</span>, {}, [
      <span class="hljs-string">`<span class="hljs-subst">${post.emoji || <span class="hljs-string">&#x27;📝&#x27;</span>}</span> `</span>,
      <span class="hljs-title function_">e</span>(<span class="hljs-string">&#x27;a&#x27;</span>, { <span class="hljs-attr">href</span>: post.<span class="hljs-property">link</span>.<span class="hljs-title function_">replace</span>(<span class="hljs-string">&#x27;https://pfy.ch&#x27;</span>, <span class="hljs-string">&#x27;&#x27;</span>) }, [
        <span class="hljs-title function_">e</span>(<span class="hljs-string">&#x27;span&#x27;</span>, {}, [
          <span class="hljs-title function_">e</span>(
            <span class="hljs-string">&#x27;span&#x27;</span>,
            { <span class="hljs-attr">className</span>: <span class="hljs-string">&#x27;date&#x27;</span> },
            <span class="hljs-string">`<span class="hljs-subst">${post.date.toLocaleDateString()}</span> - `</span>,
          ),
          post.<span class="hljs-property">title</span>.<span class="hljs-title function_">replaceAll</span>(<span class="hljs-string">&#x27;&amp;amp;&#x27;</span>, <span class="hljs-string">&#x27;&amp;&#x27;</span>),
        ]),
      ]),
    ]),
  ),
);
</code></pre> ]]></content>
                </entry><entry>
                  <id>https://pfy.ch/music/scrobbler.html</id>
                  <title>Rockbox Scrobbler</title>
                  <published>2025-03-15T03:19:43.291Z</published>
                  <updated>2026-02-22T00:22:48.249Z</updated>
                  <link>https://pfy.ch/music/scrobbler.html</link>
                  <emoji>🎵</emoji>
                  <content type="html" xml:base="https://pfy.ch/music/scrobbler.html"><![CDATA[ <p>I maintain this tool in my spare time. <a class=""  href="../donate.html">Please consider donating</a> to keep this site and tool running if you can spare a few dollars!</p>
<p><last-fm-scrobbler></last-fm-scrobbler></p>
<hr>
<details>
    <summary>Where is my log file?</summary>

<p>If you are on a daily build of RockBox you will need to export your <code>.scrobbler.log</code> file manually from the LastFM Plugin. It will then be in it&#39;s the usual location. After uploading - delete the <code>.scrobbler.log</code> as usual.</p>
</details>

<details>
    <summary>Having issues submitting?</summary>

<blockquote>
<p><strong>Note:</strong> A common issue identified with some users is their devices datetime being incorrect!<br>Please ensure your device has the <strong><em>correct datetime</em></strong> set before submitting any issues!</p>
</blockquote>
<p>I&#39;ve had some reports of <em>some</em> devices not creating valid <code>.scrobbler.log</code> files! If you run into this issue please <a class=""  href="mailto:contact@pfy.ch">email me</a> a copy of the file, as well as any notes such as:</p>
<ul>
<li>Did all songs fail to submit or just some?</li>
<li>What device are you using</li>
<li>What version of Rockbox are you running.</li>
</ul>
<p>I maintain this tool in my spare time! So please be patient if it takes me some time to get back to you.<br>If my tool has helped you <a class=""  href="../donate.html">please consider donating</a> to keep this site running!</p>
</details>
<details open>
    <summary>Update notes</summary>

<h3 id="update-2025-12-14">Update 2025-12-14</h3>
<ul>
<li>Rewritten to align with new code-style &amp; <a class="externalLink" target="_blank" href="https://git.pfy.ch/pfych/static/src/branch/master/packages/site/assets/scripts/components/scrobbler.tsx">Open sourced</a></li>
<li>Uploads songs in batches of 25 instead of individually</li>
</ul>
<h3 id="update-2025-03-21">Update 2025-03-21</h3>
<ul>
<li>Handles scrobbling new format <code>.scrobbler.log</code> files from daily RockBox builds</li>
<li>No longer hides scrobbled songs once completed</li>
</ul>
</details>

<p><bsky-comments data-post-id="3lkf6kkmddk2h"></bsky-comments></p>
 ]]></content>
                </entry><entry>
                  <id>https://pfy.ch/programming/bluesky-comments.html</id>
                  <title>Bluesky Comments</title>
                  <published>2025-02-24T06:51:09.058Z</published>
                  <updated>2025-10-06T05:45:00.000Z</updated>
                  <link>https://pfy.ch/programming/bluesky-comments.html</link>
                  <emoji>💻</emoji>
                  <content type="html" xml:base="https://pfy.ch/programming/bluesky-comments.html"><![CDATA[ <p>I saw a <a class="externalLink" target="_blank" href="https://beej.us/blog/data/mastodon-comments/">really cool implementation</a> of a comment system on someones site using their mastodon instance. I also run a mastodon instance, but none of my friends <em>actually use it</em>. Which would make it harder for them to comment on my posts.</p>
<p>A lot of them have moved to Bluesky from Twitter, and I heard it&#39;s pretty open with it&#39;s API so I thought I&#39;d give it a try doing something similar, but with it instead of Mastodon.</p>
<p>There&#39;s an API endpoint for getting a thread at <code>/xrpc/app.bsky.feed.getPostThread</code> but it requires a <code>uri</code> parameter. This URI parameter is made up of my user <code>did</code>, the <code>collection</code> and the object <code>id</code>. Making the final pattern something like this:</p>
<pre><code class="hljs">at://&lt;did&gt;/&lt;collection&gt;/&lt;id&gt;
</code></pre><p>I know the collection for posts is <code>app.bsky.feed.post</code> from reading through their docs, and the object id is just the characters at the end of the post URL, but getting the user did was a bit harder.</p>
<p>The API endpoint <code>/xrpc/app.bsky.actor.getProfile</code> allows you to get a bunch of information about a user, including their <code>did</code>. It&#39;s just passed as a query parameter.</p>
<pre><code class="hljs">/xrpc/app.bsky.actor.getProfile?actor=pfy.ch
</code></pre><p>Which returns my did:</p>
<pre><code class="hljs">did:plc:lpxtxymhlfbs5foxba7mfewt
</code></pre><p>So - to get this posts comments I&#39;ll pass this AT-URI:</p>
<pre><code class="hljs">at://did:plc:lpxtxymhlfbs5foxba7mfewt/app.bsky.feed.post/3livnrzudt22s
</code></pre><p>and it returns all replies to that post!</p>
<p>I dont handle embeds, threads or links yet - only top level replies. But it should be pretty easy to add those if I decide to down the line. Overall it was surprisingly easy to get working &amp; I&#39;ll likely start posting about my stuff here more often on Bluesky since they&#39;re now intertwined with my website. Really hoping that Bluesky doesnt get enshittified and close their API down in 5 years. 🤞</p>
<p><bsky-comments data-post-id="3livnrzudt22s"></bsky-comments></p>
 ]]></content>
                </entry><entry>
                  <id>https://pfy.ch/programming/deluge-gluetun-docker.html</id>
                  <title>Tracker unreachable when running Deluge with Gluetun in Docker</title>
                  <published>2025-02-08T11:28:21.000Z</published>
                  <updated>2025-02-08T11:40:27.000Z</updated>
                  <link>https://pfy.ch/programming/deluge-gluetun-docker.html</link>
                  <emoji>💻</emoji>
                  <content type="html" xml:base="https://pfy.ch/programming/deluge-gluetun-docker.html"><![CDATA[ <p>I&#39;ve been having an issue with my Deluge instance where random torrents on random trackers would either throw &quot;skipping tracker announce (unreachable)&quot; or &quot;Tracker Error: timed out&quot;. It&#39;s a super generic error, and most of the time it&#39;s a temporary issue or an issue caused by user configuration, so it&#39;s really difficult to find solutions online.</p>
<p><img src="https://imgs.xkcd.com/comics/wisdom_of_the_ancients.png" alt="90% of threads are like this" /></p>
<p>&nbsp;</p>
<p><strong>However</strong>, I stumbled upon <a class="externalLink" target="_blank" href="https://forum.deluge-torrent.org/viewtopic.php?t=56331">this form thread</a> where <a class="externalLink" target="_blank" href="https://forum.deluge-torrent.org/viewtopic.php?p=235522&sid=db06cebbbb44b997d2bca45b1b2373b2#p235522">*mr.lite.touch* provides a solution!</a></p>
<ol>
<li>Install the <a class="externalLink" target="_blank" href="https://github.com/ratanakvlun/deluge-ltconfig">LTConfig</a> plugin in Deluge</li>
<li>Run <code>ip addr show</code> in both your Deluge &amp; Gluetun containers. </li>
<li>Both containers should have a <code>tun0</code> interface with an IPv4 address that is the same. Note it down.</li>
<li>In Deluge, open Preferences &gt; LTConfig</li>
<li>Find <code>listen_interfaces</code>. </li>
<li>Set it to <code>&lt;IP&gt;:&lt;PORT&gt;</code> where the IP is the IPv4 address of your <code>tun0</code> interface &amp; the Port is whatever was there before (<code>42676</code> in my case)</li>
<li>Click &quot;Apply&quot; and &quot;Ok&quot;</li>
<li>Select all errored torrents and right-click &gt; Update Tracker</li>
</ol>
<p>From what I understand, this forces libTorrent to use a specific interface when listening for tracker connections. By default, libTorrent will listen on all interfaces, which <em>I think</em>  is why it wasn&#39;t working before &amp; would randomly throw these errors.</p>
<pre><code class="hljs language-mermaid">flowchart LR
    subgraph Container 
        Deluge -- &amp;nbsp;tun0&amp;nbsp; --&gt; Gluetun
    end
    
    Gluetun -- &amp;nbsp;tun0&amp;nbsp; --&gt; WAN
</code></pre><p>These changes immediately fix the issue for me. Hoping that it doesn&#39;t come back...</p>
 ]]></content>
                </entry><entry>
                  <id>https://pfy.ch/games/mahjong/2-3p-mahjong.html</id>
                  <title>2 &amp; 3 Player Riichi Mahjong</title>
                  <published>2025-02-01T02:26:32.000Z</published>
                  <updated>2025-02-01T02:29:27.000Z</updated>
                  <link>https://pfy.ch/games/mahjong/2-3p-mahjong.html</link>
                  <emoji>🎮</emoji>
                  <content type="html" xml:base="https://pfy.ch/games/mahjong/2-3p-mahjong.html"><![CDATA[ <p>My partner and I both play Mahjong online, but sometimes it&#39;s nice to play in-person. This is how we play Riichi Mahjong with less than 4 players (It&#39;s basically <a class="externalLink" target="_blank" href="https://riichi.wiki/Sanma">Sanma</a> after doing more research...).</p>
<h2 id="rules">Rules</h2>
<ul>
<li>Each player starts with 25,000 points</li>
<li>2-8 manzu tiles are removed (only 1 &amp; 9 are used from the set), all other tiles are used</li>
<li>Non-player seat winds count as Dora <em>(optional)</em>. When drawn, the player can call &quot;kita&quot; and draw from the dead wall. Kita calls can be ronned, but do not award <a class="externalLink" target="_blank" href="https://riichi.wiki/Chankan">chankan</a>. Players can still call riichi. </li>
<li>Red 5&#39;s are used</li>
<li>Chii is not allowed</li>
</ul>
<h2 id="setup">Setup</h2>
<ol>
<li>Shuffle all the tiles</li>
<li>Create 4 walls of 13-14-13-14 length </li>
<li>Dealer rolls 2 dice and breaks the corresponding wall</li>
<li>Count 3 to the left of the dead wall and add those tiles</li>
<li>Draw as normal</li>
</ol>
 ]]></content>
                </entry><entry>
                  <id>https://pfy.ch/donate.html</id>
                  <title>Donations</title>
                  <published>2025-01-25T00:00:00.000Z</published>
                  <updated>2025-06-21T07:47:35.000Z</updated>
                  <link>https://pfy.ch/donate.html</link>
                  <emoji>📝</emoji>
                  <content type="html" xml:base="https://pfy.ch/donate.html"><![CDATA[ <div id="success">
Thank you for your donation!! Please contact me if you would like your name, site, 88x31, etc. listed on this page!
</div>

<p>If you would like to support me and what I do with this site and elsewhere online please click the following link to make a donation via Stripe, all purchases are in AUD and are completed via Stripe Checkout:</p>
<p><a class="externalLink" target="_blank" href="https://buy.stripe.com/5kQfZg6mv0qA1kidSi1kA00">Choose your price!</a></p>
<p>You can also donate with Cryptocurrency:</p>
<ul>
<li><strong>ETH:</strong> <ul>
<li><pre><code class="hljs">0xE1f27cbcE0576b114789660Ccc3C2B3fb932569D
</code></pre></li>
</ul>
</li>
<li><strong>XMR:</strong> <ul>
<li><pre><code class="hljs">47Ysk63QvQ6GKQ4wF7fQkpAhhShjobAHtTdpkceSaqWUSB48XLzs5ycRcBqoktu2NVLXwMvsNkdWKgNa9x5TsbqNSs3YTBT
</code></pre></li>
</ul>
</li>
</ul>
<hr>
<p>Thank you to the following people for either donating or reaching out with kind words!</p>
<ul>
<li>Maxime, regarding my <a class=""  href="programming/func-godot.html">Godot tutorial</a></li>
<li>Jeff, regarding <a class=""  href="programming/disable-samsung-game-bar.html">Samsung monitors</a></li>
<li>Thomas, regarding <a class=""  href="programming/disable-samsung-game-bar.html">Samsung monitors</a></li>
</ul>
<style>
#success {
    display: none;
}

#success p {
    margin-bottom: 0;
    font-weight: bold;
    font-size: 18px;
    line-height: 18px;
}

#success:target {
    display: block;
    text-align: center;
    padding: 12px;
    border: 1px dashed var(--colour-text);
    margin-bottom: 12px;
}
</style>
 ]]></content>
                </entry><entry>
                  <id>https://pfy.ch/games/mahjong/player-guide.html</id>
                  <title>Riichi Mahjong Player Guide</title>
                  <published>2025-01-09T21:48:13.032Z</published>
                  <updated>2025-05-03T02:12:21.000Z</updated>
                  <link>https://pfy.ch/games/mahjong/player-guide.html</link>
                  <emoji>🎮</emoji>
                  <content type="html" xml:base="https://pfy.ch/games/mahjong/player-guide.html"><![CDATA[ <p>This guide is primarily use as a quick refresher for new players I play with in-person. If I&#39;ve sent this to you, hopefully you&#39;ll have some fun! Dont worry too much about scoring - we&#39;re (hopefully) not gambling!</p>
<ul>
<li><a class=""  href="#the-tiles">The Tiles</a></li>
<li><a class=""  href="#basic-hands">Basic Hands</a></li>
<li><a class=""  href="#on-your-turn">On your turn</a></li>
<li><a class=""  href="#yaku-scoring">Yaku & Scoring</a></li>
</ul>
<h2 id="the-tiles">The Tiles</h2>
<p>The Sticks suit (Usually call &quot;souzu&quot;), The 1 of sticks is usually a bird.</p>
<div class="mpszhand">
123456789s
</div>

<p>The Pin suit (Usually called &quot;pinzu&quot;)</p>
<div class="mpszhand">
123456789p
</div>

<p>The Number suit (Usually called &quot;manzu&quot;)</p>
<div class="mpszhand">
123456789m
</div>

<p>Honor tiles, consisting of 4 winds (East, South, West &amp; North) and 3 dragons (White, Green &amp; Red)</p>
<div class="mpszhand">
1234567z
</div>

<h2 id="basic-hands">Basic Hands</h2>
<p>The goal of Mahjong is to create a hand with 14 titles. This hand contains 4 sets of 3 and a pair. Like so:</p>
<div class="mpszhand">
123s 678s 777p 123m 77z
</div>

<p>Sets of 3 can either be a sequence or three of a kind.</p>
<p><strong>Three of a kind:</strong></p>
<div class="mpszhand">
444m
</div>

<p><strong>Sequence:</strong></p>
<div class="mpszhand">
567s
</div>

<p>(You <em>cannot</em> make a sequence out of the honor tiles like dragons or winds!)</p>
<h2 id="on-your-turn">On your turn</h2>
<p>Each turn begins with you drawing a tile from the wall, this draw should bring your hand to 14 titles.
Once drawn, you can either:</p>
<ol>
<li>Declare that the drawn tile won you the game (Tsumo)</li>
<li>Discard a tile from your hand, returning it to 13 tiles</li>
</ol>
<p>When you discard a tile, other players have the option of &quot;stealing&quot; it.</p>
<p><strong>Any player</strong> can take the tile to make 3 of a kind (Pon)<br><strong>Only the player after you</strong> can take the tile to make a sequence (Chi)</p>
<p>If a player takes the tile, they must place the sequence or 3 of a kind to their right open for everyone to see. </p>
<p>This continues until somebody either draws a tile to complete their hand (Tsumo) or they &quot;steal&quot; the tile from someone to complete their hand (Ron).</p>
<p>You can steal for <em>3 of a kind</em> from <strong>any</strong> player, but you can only take a <em>sequence</em> from the player <strong>before you</strong>.<br>You can steal for <em>anything</em> if it <strong>wins you the game</strong>.</p>
<h2 id="yaku-scoring">Yaku & Scoring</h2>
<p>Riichi Mahjong scoring can be <a class="externalLink" target="_blank" href="https://riichi.wiki/Japanese_mahjong_scoring_rules">a little confusing</a> - especially for new casual players.</p>
<p>You start with <a class="externalLink" target="_blank" href="https://riichi.wiki/Tenbou">25,000 points</a>, the game ends after each player has been dealer once &amp; if someone has 30,000 points, <strong>or</strong> if someone has negative points. </p>
<p>The value of a hand is determined by the amount of Yaku &amp; Fu. As a beginner you should use a <a class=""  href="scoring-calculator.html">scoring calculator</a>, but it&#39;s important to know some basic Yakus&#39; since you&#39;ll need at least 1 in your hand to score points.</p>
<p>Here&#39;s a few easy to remember Yakus:</p>
<ul>
<li><strong>Menzenchin tsumohou</strong> (Closed hand)<ul>
<li>Not stealing any tiles, and drawing your winning tile.</li>
</ul>
</li>
<li><strong>Riichi</strong> (Closed Hand)<ul>
<li>Announce you are 1 tile from winning with a closed hand. You must discard all draws unless they win you the game. You can win from Drawing or &quot;stealing&quot; (Ron)</li>
</ul>
</li>
<li><strong>Tanyao</strong> (Can be open or closed)<ul>
<li>Win without 1, 9 or Honor tiles in your hand.</li>
</ul>
</li>
</ul>
<div class="mpszhand">
567333s444p67822m
</div>

<ul>
<li><strong>Yakuhai</strong> (Can be open or closed)<ul>
<li>Have at least 1 set of dragons, Round winds, or Seat winds in your hand.</li>
</ul>
</li>
</ul>
<div class="mpszhand">
567333s444p22m777z
</div>

<ul>
<li><strong>Toitoi</strong> (Can be open or closed)<ul>
<li>All 3 of a kind &amp; 1 pair</li>
</ul>
</li>
</ul>
<div class="mpszhand">
111777m111z888p44s
</div>

<br />

<p>If you won by drawing (Tsumo) the point value of your hand is taken from everyone&#39;s points, If you won by stealing (Ron) the point value of your hand is taken from <em>that players points</em>. If you think someone is about to win make sure you dont discard the tile they need!</p>
 ]]></content>
                </entry><entry>
                  <id>https://pfy.ch/programming/projects/mpsz-rendering.html</id>
                  <title>Mahjong MPSZ Rendering</title>
                  <published>2024-12-30T21:59:34.939Z</published>
                  <updated>2025-01-09T21:48:08.000Z</updated>
                  <link>https://pfy.ch/programming/projects/mpsz-rendering.html</link>
                  <emoji>💻</emoji>
                  <content type="html" xml:base="https://pfy.ch/programming/projects/mpsz-rendering.html"><![CDATA[ <p>I&#39;ve written a fun script to render out tiles using <a class="externalLink" target="_blank" href="https://tug.org/docs/latex/mahjong/mahjong-code.pdf">mpsz notation</a> &amp; a library of <a class="externalLink" target="_blank" href="https://github.com/FluffyStuff/riichi-mahjong-tiles">public domain SVGs</a>.</p>
<p>It takes the following plain text: <code>119p 19s 19m 1234567z</code><br>Then renders out the following:</p>
<div class="mpszhand">
119p 19s 19m 1234567z
</div>

<p>Currently, it just parses my markdown files on my site and renders out the tiles at runtime, but I&#39;ll eventually make it an NPM package if I get around to it.</p>
<h3 id="interactive-demo">Interactive demo</h3>
<p>Update the input below to view your hand. Use MPSZ notation.</p>
<div class="mpszdemo">
If you have Javascript enabled, the demo should render here.
</div>

<h3 id="code-sandbox">Code Sandbox</h3>
<p>I&#39;ve put a small amount of the code into a sandbox for you to poke around with. I actually discovered a few bugs with my sandbox implementation, so I&#39;ll eventually need to fix those... This code is in plain JS and not Typescript - which the main implementation is. Another limitation of my sandbox sadly.</p>
<iframe
width="100%"
style="aspect-ratio: 1/1"
src="/sandbox.html?data=eJztWW2T0zYQ%2FivCtNguiZN75c65HFMo7bUFyvQYysxdPii2kohzbI8l33FAZvo3%2BqF%2Frr%2Bku7Idyy%2FxXaEvfKgAJdazWu0%2Bu5ZW4b2xkMvAcI0jn18SL6BCjM%2BNq4TGMUvOjePzUAHch1EYUyPn8ig%2Bfhxw74KcGz%2BnIXmRRG%2BYJ88NwkMiF4zIKCYBm8mjQYwaBqACP%2BPjExr6xKMhmTLCfC6ZT6hUU6aRlNGSRDP19MMp8SKfERFyMETeOZomZHD8kgcwxN%2Bxmop81dPHp6eOvqTRMzwhwLkvJMw8YXy%2BkC7Z343fjsCL0MndJO%2FRp5j6Pg%2FnLtnaVvgKRe6CAMKE%2BFzEAb12ySxgCBP1pY8qXIK9Gpvjk0cDz7r7Xlt0RQbkwFYSNODzsA9mL4VLplSwgIdMIUuazHnYz4hYm4F%2FCHFQ1y%2BluYTkY8UDIXEkuORR6JKEBVTyy0wrtkXuuWbRGqMihtD1E5gRuWRnsDvKVsR2z11QYfHl3JkGNLywy8Wa8w5GZDAgJzTIAqiiBMGkJIySJQ0ILl3MXpVLgHJda%2BkEnYooSGXpBMGscslQG8AUq45ccV8ugLvh8EtttPC%2FNhxNMWv7Mw6QF4WS8lBzHt0P5aIfzfryOmbWdsV9Agn7tg9eqpSZRonPEgjd25Eusk6pnVgHVhoPFTruOUkkKWb0eqVakNceVkNZwjU%2BgbSEhmIGMYC8UMqt%2FuHQZ3M7QyBT2GtLZWxbypKvSH%2FLtm9nvJDUu%2FjcjS8fIF0fJRAesd6yCgeU2TJS47MEMkPPiRlPhCyyompuw%2F5P82C3xQMtAK2xUB38ha3vDe58kNdCEpFyKciYnJnC7BEzxm6J3TtzAsozoUvYmvxXNEhZJjpEgS3strHbwW4Xuz3s9rF7gN0BdofY9bF7jd1DpbfQjG6dvvruaRRdpDHozjmCDVBja%2BgSc4CSYnAapXv9b6KEOuJybvZKma2KzFYd3q7A23V4pwLv1OHdCrxbh%2Feq9tXh%2FQq8X4cfVOAHdfigAh%2FU4cMKfKjDq%2Fwz3kTmCx7eSCbIdJEJcBeZAHeRCXAXmWhfB5kAd5EJcBeZAHeRCXArmctNZD6jN5MJMl1kAtxFJsBdZALcRSba10EmwF1kAtxFJsBdZALcSua7TWQ%2BwqKig8iXUdjB43PaQPUXfEEbMdJzkvEOFk%2FoRdrB4gmVooFrND5epA3TDm5w%2B7ADRyJXajvFCosFWAPO0tDDQonA2QIFXwjlBxxlhAVsyUIpip2XwW5rSTrvQZktEz6Fgkr0iLfggQ%2BTcJuf2GR8XAQon5QpAdSPvBS%2FOl7C4KR6kgGosDiTflI1lAOjCWfCKlexHbDsCfUWlnV2wa57eLykrLJauaJHhfyRXcOKIKqddrklZzk%2BAQGlJhdZFVbwGbG%2BThJ67XChPq3CRbtStRWjpW1qpG6UtrSDJXfo53J6KVE85GcvC0SlGmibj%2FZUZyVMpklYCI%2BKOGesxDQRDG9Mp1IFF0K5gKdmwGY8pEF5ck%2FyNQIGOmBxmFsF9blwq%2BFSXcvGBLU7asAyzcJSTDALVXGQGI7g46ic5AQsnMsFjN6%2FbzfjqgqpcSl%2Bxsvli7ip2sThoRekPmQQTrHtejQ0D504FQurCmNzHKfirLOksWXlQ4oz670m1FNFkZuZuLLtXlWjXbETWyuT62zI4o%2F%2BaGXUTV5lJKUpR%2FKfUblwoCb0o6VlOzLKgm7t7NuOgLs2XkBG1ekYlJBd4a24hqAdGffjMYFCrrEytnwq1mNwv3eVHflr6mZFHr2gQFCSAkG1BVaNfL%2BV1ozsuq460yorypQh98nWRHlybpjnBvnwgbSi5h%2B%2F%2FtbpqbOMfD7jsHuCcH7TMtsd22gCGGB2WfD77S3IbxsNCzoTL8v%2BXF3LvWw9u%2FHG1N%2BPMsPzTUibUduIsvMFVxTqPMEvPSJpMmdS342yEcj6kCUnL589RS%2FN9TJtG4lSdbtNRInWN5DKgdV4K8UVl94iM9hRKdh8BSlEGy4ubjNmhdZNGw42IPWsHVEaLBOuhPAitWSE3kTidRcfbU39VvecLvFlVT%2FPdE1Y1fe3ok1axuv7DLYpFAAXozbuXn9G3D2CN%2Bo23NEg3%2FmzrPhviHv4GRH3Lf7K8S8x99FGthbO%2F5CRnxJen81oCiY0pfFYuaMMw%2BKj9aTAhrtaBEJXNAkt83mkShW1f%2BLcO0CbrO3%2BN5mkfK0fLNj%2Bz7emkZWfq87W4ZpkX6EoIg%2BxqCK6YW358jf78bEpuWoemPr%2FJoyRHJ9fIjmVQ0UTMsvrTlXJk%2FVVsUMJzO6RM8iiItkm1eq6qFa16misVWiN10QzzFFrPeUCLlq%2Bb5VzNvm%2FYa2iFvsra63njDZPyW9%2FGlWgJQrZ88hnFlbWm3%2Be7lajT8vLLk0on6ddNrNyTivkrNrt0jK3tnfE7t5%2BbNq98uIPivMVH11%2F71v5fwKC1UbPUDWw4Rp7Q3xA4g33zNiCB%2Fw3NCarPwEeybLa" />

 ]]></content>
                </entry><entry>
                  <id>https://pfy.ch/games/mahjong/mahjong.html</id>
                  <title>Mahjong</title>
                  <published>2024-12-30T21:59:34.939Z</published>
                  <updated>2025-02-01T05:17:59.000Z</updated>
                  <link>https://pfy.ch/games/mahjong/mahjong.html</link>
                  <emoji>🎮</emoji>
                  <content type="html" xml:base="https://pfy.ch/games/mahjong/mahjong.html"><![CDATA[ <p>I&#39;ve gotten into playing Mahjong since I had some friends over who were into it. Both my partner and I have gotten hooked and we&#39;ve both been playing online for a while now.</p>
<p><img src="https://assets.pfy.ch/md/DSCF0271.jpg" alt="" /></p>
<p>We mostly play online on Riichi City, but sometimes play on Mahjong Soul if a friend prefers that client. I&#39;ve also picked up my own AMOS Masters set to play at home or if other people are over since I cant always rely on somebody to bring their set with them.</p>
<p>We primarily play standard Riichi Mahjong rules, but if we&#39;ve got a beginner with us we&#39;ll usually simplify scoring &amp; get rid of the 1 yaku hand minimum. It makes the game a lot more fun for people who are new to the game. </p>
<ul>
<li><a class=""  href="../../programming/projects/mpsz-rendering.html">Rendering using MPSZ Notation</a></li>
<li><a class=""  href="player-guide.html">New player guide</a></li>
<li><a class=""  href="2-3p-mahjong.html">2 & 3 Player Riichi Mahjong</a></li>
<li><a class=""  href="scoring-calculator.html">Scoring Calculator</a></li>
</ul>
 ]]></content>
                </entry><entry>
                  <id>https://pfy.ch/programming/disable-samsung-game-bar.html</id>
                  <title>Disabling Samsung display game bar popups</title>
                  <published>2024-12-15T07:28:22.685Z</published>
                  <updated>2024-12-15T08:22:26.000Z</updated>
                  <link>https://pfy.ch/programming/disable-samsung-game-bar.html</link>
                  <emoji>💻</emoji>
                  <content type="html" xml:base="https://pfy.ch/programming/disable-samsung-game-bar.html"><![CDATA[ <p>I recently purchased the 32&quot; Odyssey OLED G8 UHD Gaming Monitor<sup><a id="footnote-ref-1" href="#footnote-1" data-footnote-ref aria-describedby="footnote-label">1</a></sup>. My major gripe with this monitor is the <em>frustrating</em> menus and popups. Especially the Game Mode &quot;quick settings&quot; that will cover the bottom half of your screen on <strong><em>EVERY</em></strong> startup.</p>
<p>I found this guide<sup><a id="footnote-ref-2" href="#footnote-2" data-footnote-ref aria-describedby="footnote-label">2</a></sup> on GitHub and will summarise it here, as well as include some extra steps it missed outlined in this forum thread<sup><a id="footnote-ref-3" href="#footnote-3" data-footnote-ref aria-describedby="footnote-label">3</a></sup>.</p>
<p>You&#39;ll need python installed since this is only possible with the service remote which does not come with the display.</p>
<h2 id="steps">Steps</h2>
<ol>
<li>Connect the monitor to your wi-fi</li>
<li>Get your monitors IP address and write it down<ol>
<li>Settings</li>
<li>All Settings</li>
<li>Connection</li>
<li>Network</li>
<li>Network Status</li>
<li>IP Status</li>
</ol>
</li>
<li>Create a new Python project on your system &amp; install dependencies:</li>
</ol>
<pre><code class="hljs language-bash"><span class="hljs-built_in">mkdir</span> monitor-settings
<span class="hljs-built_in">cd</span> monitor-settings
python -m venv <span class="hljs-built_in">env</span> <span class="hljs-comment"># Optional</span>
./env/bin/pip3 install <span class="hljs-string">&quot;git+https://github.com/xchwarze/samsung-tv-ws-api.git#egg=samsungtvws[async,encrypted]&quot;</span>
</code></pre><ol start="4">
<li>Create <code>script.py</code> and add the following content, update the IP with your monitors IP:</li>
</ol>
<pre><code class="hljs language-python"><span class="hljs-keyword">import</span> sys
<span class="hljs-keyword">import</span> os
<span class="hljs-keyword">import</span> time
<span class="hljs-keyword">from</span> samsungtvws <span class="hljs-keyword">import</span> SamsungTVWS

sys.path.append(<span class="hljs-string">&#x27;../&#x27;</span>)
token_file = os.path.dirname(os.path.realpath(__file__)) + <span class="hljs-string">&#x27;/tv-token.txt&#x27;</span>
tv = SamsungTVWS(host=<span class="hljs-string">&#x27;192.168.X.X&#x27;</span>, port=<span class="hljs-number">8002</span>, token_file=token_file)

info = tv.rest_device_info()

<span class="hljs-built_in">print</span>(info)

tv.send_key(<span class="hljs-string">&quot;KEY_INFO&quot;</span>)
time.sleep(<span class="hljs-number">0.3</span>)
tv.send_key(<span class="hljs-string">&quot;KEY_FACTORY&quot;</span>)
</code></pre><ol start="5">
<li>Run the script</li>
<li>Using the remote allow the connection</li>
<li>Navigate the service menu<ol>
<li>FMS</li>
<li>FMS</li>
<li>Factory Menu App</li>
<li>Option</li>
<li>MRT Option</li>
<li><code>com.samsung/featureconf/game_bar.auto_launch_support_type</code></li>
<li><code>FMS_AUTO_LAUNCH_NOT_SUPPORT</code></li>
</ol>
</li>
<li>Turn the display off</li>
<li>Turn the display on</li>
<li>Disconnect your monitor from wi-fi</li>
</ol>
<p>This should successfully disable the game bar popups on monitor startup/wake. If your monitor updates when you try to power back on you may need to run through these steps again.</p>
<p>It&#39;s extremely frustrating that this option is not a <em>default</em> toggle in the general settings. The panel on this display is excellent when gaming and <em>ok</em> for general purpose (text fringing 😭) but this menu, the fact there&#39;s apps &amp; the terrible remote. It feels like I bought a shit smart TV with a really nice OLED panel.</p>
<p>I would not recommend this display to anybody for those reasons alone.</p>
<p><img src="/guide-images/smasnug/monitor-0.png" alt="I got this cause rtings rate it the best OLED gaming monitor on the market rn and it was 25% off" /></p>
<p><img src="/guide-images/smasnug/monitor-1.png" alt="It sounds like you got a small smart TV lmao" /></p>
<p><img src="/guide-images/smasnug/monitor-2.png" alt="lmaoooo I would never buy a monitor that can connect to the internet" /></p>
<section class="footnotes" data-footnotes>
<h2 id="footnote-label" class="sr-only">Footnotes</h2>
<ol>
<li id="footnote-1">
<p>The displays SKU is LS32DG802SEXXY <a href="#footnote-ref-1" data-footnote-backref aria-label="Back to reference 1">↩</a></p>
</li>
<li id="footnote-2">
<p><a class="externalLink" target="_blank" href="https://github.com/mainde/samsung-odyssey-G8-game-bar-overlay-fix">GitHub gist with instructions</a> <a href="#footnote-ref-2" data-footnote-backref aria-label="Back to reference 2">↩</a></p>
</li>
<li id="footnote-3">
<p><a class="externalLink" target="_blank" href="https://www.avsforum.com/threads/2022-202x-samsung-service-menu-access-details-tips-tricks-mods.3299009/">Forum thread with instructions</a> <a href="#footnote-ref-3" data-footnote-backref aria-label="Back to reference 3">↩</a></p>
</li>
</ol>
</section>
 ]]></content>
                </entry><entry>
                  <id>https://pfy.ch/music/ipod.html</id>
                  <title>Daily driving an iPod classic</title>
                  <published>2024-11-27T05:42:56.949Z</published>
                  <updated>2025-01-10T06:44:39.000Z</updated>
                  <link>https://pfy.ch/music/ipod.html</link>
                  <emoji>🎵</emoji>
                  <content type="html" xml:base="https://pfy.ch/music/ipod.html"><![CDATA[ <banner-element>

<p><strong>Update 2025/01/10:</strong> I fixed my issue with crackling by putting some electrical tape underneath the headphone jack. However, the main fix is likely that I had the battery <em>on top</em> of the iFlash - when it should have been underneath ... My bad!</p>
</banner-element>

<p>A while back I got my hands on a 80GB 6th gen iPod classic for <em>free</em>. It still worked great but the battery life was pretty mediocre and using it with my Linux desktop was a pain.</p>
<p>I ended up installing <a class="externalLink" target="_blank" href="https://www.rockbox.org/">Rockbox</a> on the iPod, since it makes managing music easier and allows the iPod to play flac files, which most of my music is in. However, with this custom firmware, and prettier skins. It was pretty obvious that this old 80GB hard drive was <em>struggling</em>.</p>
<p>I ended up purchasing a iFlash Quad &amp; a 3000mAh Battery, with the <em>plan</em> to just swap them in, add a 256 GB MicroSD and put it all back together. <strong><em>It couldn&#39;t be that hard right?</em></strong> </p>
<p>Here&#39;s an image from the iFixit guide for this model of iPod:</p>
<p><img src="https://guide-images.cdn.ifixit.com/igi/U6UpLStuFxclrQse.large" alt="Yes, that's a paint scraper shoved between the face and back plate...[^1]" /></p>
<p>I ended up absolutely obliterating the rear case, main frame clips <em>and</em> the headphone ribbon cable... Meaning I had to order a new rear case and headphone assembly ...</p>
<p>Once those new parts arrived though, it was really easy to swap things in. Except now:</p>
<ul>
<li>The top clip on the rear case isn&#39;t fully engaged</li>
<li>The left headphone chanel is louder than the right<sup><a id="footnote-ref-2" href="#footnote-2" data-footnote-ref aria-describedby="footnote-label">2</a></sup></li>
</ul>
<p>The top clip is concerning since I don&#39;t know if the battery is being squashed or if the new back case is just not great quality, and the headphones being imbalanced is nothing Rockboxes balance settings can&#39;t fix...</p>
<p>Regardless though, I&#39;m pretty happy with the result. I&#39;ve been using it non-stop for a while now and I still haven&#39;t had to charge it. I&#39;m trying to run the battery flat so that the battery level will be more accurate once I re-charge it to full.</p>
<p>I&#39;ve loaded it with all my favourite albums and either listen to an album in its entirety or shuffle every track on the iPod. I might eventually make genre playlists, but I&#39;m trying to keep only music I want to listen to on the iPod. With my PlexAmp setup I&#39;ve got a lot of music I don&#39;t actually want to listen too and always skip. I want to avoid that with this iPod.</p>
<p>I&#39;d really recommend this setup to people who just like to listen to their own music. Don&#39;t get a 6th or 7th gen iPod though. The all metal build feels great but holy fuck it&#39;s a nightmare to open. There&#39;s probably more modern audio players but the one&#39;s I&#39;ve found are just <em>really expensive</em> Android phones with headphone jacks. I like the small form and simplicity of the iPod!</p>
<p>Check out what music is on my probably on my iPod on my <a class=""  href="./music.html">music</a> page!</p>
<section class="footnotes" data-footnotes>
<h2 id="footnote-label" class="sr-only">Footnotes</h2>
<ol>
<li id="footnote-1">
<p><a class="externalLink" target="_blank" href="https://www.ifixit.com/Guide/iPod+Classic+Battery+Replacement/561#s2685">Source</a> <a href="#footnote-ref-1" data-footnote-backref aria-label="Back to reference 1">↩</a></p>
</li>
<li id="footnote-2">
<p>My hearing might also be not great. Wear earplugs in loud environments! <a href="#footnote-ref-2" data-footnote-backref aria-label="Back to reference 2">↩</a></p>
</li>
</ol>
</section>
 ]]></content>
                </entry><entry>
                  <id>https://pfy.ch/programming/stripe.html</id>
                  <title>Stripe</title>
                  <published>2024-10-01T16:07:56.000Z</published>
                  <updated>2025-03-17T02:12:56.000Z</updated>
                  <link>https://pfy.ch/programming/stripe.html</link>
                  <emoji>💻</emoji>
                  <content type="html" xml:base="https://pfy.ch/programming/stripe.html"><![CDATA[ <p>I&#39;ve written a basic stripe checkout handler + a tiny CMS to sell stuff from my site.</p>
<p>It handles basic stock keeping - which was why I couldn&#39;t just use Stripe&#39;s hosted checkout.</p>
<p>It&#39;s not really meant for scale, but it&#39;ll handle what I need just fine - I&#39;m not expecting to be selling a billion thingamajigs anytime soon! <del>If you want to purchase the example below for $1, go ahead. I&#39;ll send you a nice email when the payment goes through. Be warned though, there&#39;s only 2 in stock! You really don&#39;t want to miss out! Haha.</del> Sold out! $2 Profit (minus tax ;_;) </p>
<div class="stripeProduct">
example
</div>

<p>I currently use this on my <a class=""  href="../donate.html">donations</a> page to accept variable donations as well - so it can handle more than just static product pricing &amp; basic inventory management!</p>
 ]]></content>
                </entry><entry>
                  <id>https://pfy.ch/programming/cloudfront-reverse-proxy.html</id>
                  <title>Cloudfront as a reverse proxy</title>
                  <published>2024-09-01T16:07:56.000Z</published>
                  <updated>2025-03-17T02:12:56.000Z</updated>
                  <link>https://pfy.ch/programming/cloudfront-reverse-proxy.html</link>
                  <emoji>💻</emoji>
                  <content type="html" xml:base="https://pfy.ch/programming/cloudfront-reverse-proxy.html"><![CDATA[ <p>I run two services on AWS. One is a static website, the other is a serverless API.</p>
<p>The static website consists of the following setup:</p>
<pre><code class="hljs language-mermaid">flowchart TD 
    User --&gt; Cloudfront
    Cloudfront -- Default Behaviour --&gt; S3
</code></pre><p>It&#39;s extremely simple, and handles global caching really easily.</p>
<p>The serverless API is a bit more complex, but it&#39;s still pretty simple.</p>
<pre><code class="hljs language-mermaid">flowchart LR
    User --&gt; APIGateway[API Gateway]
    APIGateway -- GET /bar --&gt; Lambda1[Lambda 1]
    APIGateway -- GET /foo --&gt; Lambda2[Lambda 2]
</code></pre><p>I&#39;ve just setup Mastodon recently and need to update my <code>/.well-known/webfinger</code> to point to the mastodon instance. This will let me use my root domain as my mastodon username URL. Issue is - I&#39;m already using the webfinger endpoint for OIDC...</p>
<p>So the solution is to set up a special API function that can handle this request. If It&#39;s meant for mastodon - send it there. Otherwise, return whatever else I want as JSON.</p>
<pre><code class="hljs language-mermaid">flowchart TD 
    User -- /.well-known/webfinger --&gt; Cloudfront
    Cloudfront -- Custom Behaviour --&gt; APIGateway[API Gateway]
    APIGateway --&gt; Lambda[Lambda]
</code></pre><p>The issue I was running into was my cloudfront custom behaviour wasn&#39;t working. This was due to CloudFront forwarding the <code>Host</code> header to the origin. API Gateway doesn&#39;t like this &amp; will always return a 403 <code>{&quot;message&quot;:&quot;Forbidden&quot;}</code>.</p>
<p>I fixed this by ensuring the <code>OriginRequestPolicyId</code> was set up to a policy that forwarded everything <strong><em>except</em></strong> the <code>Host</code> header. AWS Incorrectly warn that this will break API Gateway, but it does not.</p>
<p>Here&#39;s my working truncated Cloudfront distribution config <code>CacheBehaviors</code> and <code>Origins</code> are the primary changes:</p>
<pre><code class="hljs language-yaml"><span class="hljs-attr">WebsiteCloudFrontDistribution:</span>
  <span class="hljs-attr">Type:</span> <span class="hljs-string">AWS::CloudFront::Distribution</span>
  <span class="hljs-attr">Properties:</span>
    <span class="hljs-attr">DistributionConfig:</span>
      <span class="hljs-attr">Origins:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">DomainName:</span> <span class="hljs-string">${self:custom.s3Bucket}.s3.${opt:region}.amazonaws.com</span>
          <span class="hljs-attr">Id:</span> <span class="hljs-string">${self:custom.s3Bucket}.s3.${opt:region}.amazonaws.com</span>
          <span class="hljs-attr">OriginAccessControlId:</span> <span class="hljs-type">!Ref</span> <span class="hljs-string">WebsiteCloudfrontOriginAccessControl</span>
          <span class="hljs-attr">S3OriginConfig:</span>
            <span class="hljs-attr">OriginAccessIdentity:</span> <span class="hljs-string">&quot;&quot;</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">DomainName:</span> <span class="hljs-string">api.pfy.ch</span>
          <span class="hljs-attr">Id:</span> <span class="hljs-string">api.pfy.ch</span>
          <span class="hljs-attr">CustomOriginConfig:</span>
            <span class="hljs-attr">OriginProtocolPolicy:</span> <span class="hljs-string">https-only</span>
      <span class="hljs-attr">DefaultRootObject:</span> <span class="hljs-string">index.html</span>
      <span class="hljs-attr">CacheBehaviors:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">PathPattern:</span> <span class="hljs-string">/.well-known/*</span>
          <span class="hljs-attr">TargetOriginId:</span> <span class="hljs-string">api.pfy.ch</span>
          <span class="hljs-attr">ViewerProtocolPolicy:</span> <span class="hljs-string">redirect-to-https</span>
          <span class="hljs-attr">CachePolicyId:</span> <span class="hljs-string">4135ea2d-6df8-44a3-9df3-4b5a84be39ad</span> <span class="hljs-comment"># Disable caching</span>
          <span class="hljs-attr">OriginRequestPolicyId:</span> <span class="hljs-string">88de81a8-de41-4bfc-a73-d5de1f3cd023</span> <span class="hljs-comment"># Custom policy to forward everything except host!</span>
          <span class="hljs-attr">AllowedMethods:</span>
            <span class="hljs-bullet">-</span> <span class="hljs-string">GET</span>
            <span class="hljs-bullet">-</span> <span class="hljs-string">HEAD</span>
            <span class="hljs-bullet">-</span> <span class="hljs-string">OPTIONS</span>
            <span class="hljs-bullet">-</span> <span class="hljs-string">PUT</span>
            <span class="hljs-bullet">-</span> <span class="hljs-string">POST</span>
            <span class="hljs-bullet">-</span> <span class="hljs-string">PATCH</span>
            <span class="hljs-bullet">-</span> <span class="hljs-string">DELETE</span>
          <span class="hljs-attr">CachedMethods:</span>
            <span class="hljs-bullet">-</span> <span class="hljs-string">HEAD</span>
            <span class="hljs-bullet">-</span> <span class="hljs-string">GET</span>
          <span class="hljs-attr">ForwardedValues:</span>
            <span class="hljs-attr">QueryString:</span> <span class="hljs-literal">true</span>
</code></pre><p>If forwarding to API Gateway <strong><em>ENSURE</em></strong> that the <code>Host</code> header is not forwarded.</p>
 ]]></content>
                </entry><entry>
                  <id>https://pfy.ch/programming/projects/gitadora.html</id>
                  <title>Gitadora to Kamaitachi</title>
                  <published>2024-08-17T06:23:26.626Z</published>
                  <updated>2025-07-13T06:51:21.000Z</updated>
                  <link>https://pfy.ch/programming/projects/gitadora.html</link>
                  <emoji>💻</emoji>
                  <content type="html" xml:base="https://pfy.ch/programming/projects/gitadora.html"><![CDATA[ <p><a class="externalLink" target="_blank" href="https://tachi.ac">Tachi</a> is a service for managing rhythm game scores. It&#39;s had support for Gitadora for a while but only supported manual imports.</p>
<p>I&#39;ve written an userscript to scrape Gitadora scores from Konami&#39;s score page and export valid JSON which tachi can read. </p>
<p><a class=""  href="/scripts/user-scripts/gitadora.user.js">Install userscript</a> (Requires Tamper/Violentmonkey)</p>
<h2 id="guide">Guide</h2>
<ol>
<li>Navigate to <a class="externalLink" target="_blank" href="https://p.eagate.573.jp/game/gfdm/">Gitadora on the Konami website</a></li>
<li>Select &quot;プレーデータ&quot; then &quot;プレーデータ&quot; in the navbar</li>
<li>Select &quot;プレー履歴&quot; in the sidebar</li>
<li>Click either &quot;GF&quot; or &quot;DM&quot; on the page</li>
<li>Click the scrape button</li>
<li>Wait for the script to navigate to the last page of scores</li>
<li>Click the download button</li>
<li>Navigate to <a class="externalLink" target="_blank" href="https://kamai.tachi.ac/import/batch-manual">Batch Manual import page on Tachi</a></li>
<li>Select the exported file</li>
<li>Click &quot;Submit File&quot;</li>
</ol>
<p>Some scores may fail to import since Tachi does not have all Gitadora songs in its database. Kamaitachi is usually 1 major version behind. </p>
<hr>
<p><strong>Update 2024-08-21:</strong> Kamaitachi now supports up to &quot;Fuzz Up&quot;. <a class="externalLink" target="_blank" href="https://github.com/zkrising/Tachi/blob/main/seeds/scripts/rerunners/gitadora/parse-mdb.ts">I've written a script</a> to make importing new version easier so hopefully as new versions release it&#39;ll be updated.</p>
<p><strong>Update 2025-06-25:</strong> The script has been updated to version 1.4 and supports scraping the mobile version of the webpage.</p>
<p><strong>Update 2025-07-13:</strong> Fixed an issue with downloading json files on iOS</p>
 ]]></content>
                </entry><entry>
                  <id>https://pfy.ch/games/games.html</id>
                  <title>Games</title>
                  <published>2024-08-11T07:30:04.683Z</published>
                  <updated>2025-06-19T06:25:16.000Z</updated>
                  <link>https://pfy.ch/games/games.html</link>
                  <emoji>🎮</emoji>
                  <content type="html" xml:base="https://pfy.ch/games/games.html"><![CDATA[ <p>I do a <em>little</em> gaming. This used to be one big long page, but I&#39;ve split it out into game specific pages. These pages are used to keep track of any specific game achievements I think are notable or for any opinions on certain games.</p>
<h2 id="current-rotation">Current Rotation</h2>
<p>Games I&#39;m playing now or plan to play through soon.  This will stay partially up to date as I play more games.
<br /><br /></p>
<div class="noScript">

<p>With JS enabled, this renders way nicer!</p>
</div>

<div class="recommendations threeByThree">

<table>
<thead>
<tr>
<th>Title</th>
<th>Console</th>
<th>Link</th>
<th>Image</th>
</tr>
</thead>
<tbody><tr>
<td>LR2oraja</td>
<td>PC</td>
<td>bms.html</td>
<td><a class="externalLink" target="_blank" href="https://assets.pfy.ch/lr2oraja.png">https://assets.pfy.ch/lr2oraja.png</a></td>
</tr>
<tr>
<td>Beatmania IIDX</td>
<td>Arcade</td>
<td>iidx/iidx.html</td>
<td><a class="externalLink" target="_blank" href="https://assets.pfy.ch/covers/beatmania.jpg">https://assets.pfy.ch/covers/beatmania.jpg</a></td>
</tr>
<tr>
<td>Monster Hunter Double Cross</td>
<td>3DS</td>
<td></td>
<td><a class="externalLink" target="_blank" href="https://assets.pfy.ch/covers/double-cross.jpg">https://assets.pfy.ch/covers/double-cross.jpg</a></td>
</tr>
<tr>
<td>Monster Hunter Rise</td>
<td>PC</td>
<td></td>
<td><a class="externalLink" target="_blank" href="https://assets.pfy.ch/covers/rise.jpg">https://assets.pfy.ch/covers/rise.jpg</a></td>
</tr>
<tr>
<td>Final Fantasy XIV</td>
<td>PC</td>
<td></td>
<td><a class="externalLink" target="_blank" href="https://assets.pfy.ch/covers/stormblood.png">https://assets.pfy.ch/covers/stormblood.png</a></td>
</tr>
<tr>
<td>Riichi Mahjong</td>
<td>Tabletop</td>
<td>mahjong/mahjong.html</td>
<td><a class="externalLink" target="_blank" href="https://assets.pfy.ch/covers/mjs.png">https://assets.pfy.ch/covers/mjs.png</a></td>
</tr>
</tbody></table>
</div>

<h2 id="recent-retro-plays">Recent Retro plays</h2>
<p>The last 3 games I&#39;ve played that have registered on retroacheivements. You can view my profile <a class="externalLink" target="_blank" href="https://retroachievements.org/user/pfych">here</a>.<br>Clicking on these games will take you to the game page on retroachievements.org.
<br /><br /></p>
<div class="currentlyPlaying">

</div>

<h2 id="games-with-their-own-pages">Games with their own pages</h2>
<p>Below are games I&#39;ve played that have their own pages - whether It&#39;s for score tracking, review or thoughts.<br>Mostly Arcade titles &amp; Arcade-style games where tracking score is important.
<br /><br /></p>
<div class="recommendations">

<table>
<thead>
<tr>
<th>Title</th>
<th>Console</th>
<th>Link</th>
<th>Image</th>
</tr>
</thead>
<tbody><tr>
<td>Beatmania IIDX</td>
<td>Arcade</td>
<td>iidx/iidx.html</td>
<td><a class="externalLink" target="_blank" href="https://assets.pfy.ch/covers/beatmania.jpg">https://assets.pfy.ch/covers/beatmania.jpg</a></td>
</tr>
<tr>
<td>LR2oraja</td>
<td>PC</td>
<td>bms.html</td>
<td><a class="externalLink" target="_blank" href="https://assets.pfy.ch/lr2oraja.png">https://assets.pfy.ch/lr2oraja.png</a></td>
</tr>
<tr>
<td>Deathsmiles</td>
<td>Arcade</td>
<td>deathsmiles.html</td>
<td><a class="externalLink" target="_blank" href="https://assets.pfy.ch/covers/deathsmiles.jpg">https://assets.pfy.ch/covers/deathsmiles.jpg</a></td>
</tr>
<tr>
<td>Dodonpachi DaiFukkatsu</td>
<td>PC/Arcade</td>
<td>dodonpachi.html</td>
<td><a class="externalLink" target="_blank" href="https://assets.pfy.ch/covers/DaiFukkatsu.jpg">https://assets.pfy.ch/covers/DaiFukkatsu.jpg</a></td>
</tr>
<tr>
<td>Crimson Clover world EXplosion</td>
<td>PC</td>
<td>crimson-clover-world-explosion.html</td>
<td><a class="externalLink" target="_blank" href="https://assets.pfy.ch/covers/Crimson">https://assets.pfy.ch/covers/Crimson</a> Clover world EXplosion.png</td>
</tr>
<tr>
<td>Touhou</td>
<td>PC</td>
<td>touhou/touhou.html</td>
<td><a class="externalLink" target="_blank" href="https://assets.pfy.ch/covers/touhou.png">https://assets.pfy.ch/covers/touhou.png</a></td>
</tr>
<tr>
<td>Psyvariar 2 - Ultimate Final</td>
<td>PlayStation 2</td>
<td>psyvariar-2-ultimate-final.html</td>
<td><a class="externalLink" target="_blank" href="https://assets.pfy.ch/covers/Psyvariar">https://assets.pfy.ch/covers/Psyvariar</a> 2 - Ultimate Final.jpg</td>
</tr>
<tr>
<td>Azur Lane</td>
<td>Mobile</td>
<td>azur-lane.html</td>
<td><a class="externalLink" target="_blank" href="https://assets.pfy.ch/covers/azur">https://assets.pfy.ch/covers/azur</a> lane.png</td>
</tr>
</tbody></table>
</div>

<h3 id="direct-links-to-game-pages">Direct links to game pages</h3>
<ul>
<li>Rhythm Games<ul>
<li><a class=""  href="iidx/iidx.html">Beatmania IIDX</a></li>
<li><a class=""  href="bms.html">LR2oraja</a></li>
</ul>
</li>
<li>Shmups<ul>
<li><a class=""  href="deathsmiles.html">Deathsmiles</a></li>
<li><a class=""  href="dodonpachi.html">Dodonpachi</a></li>
<li><a class=""  href="crimson-clover-world-explosion.html">Crimson Clover world EXplosion</a></li>
<li><a class=""  href="touhou/touhou.html">Touhou</a></li>
<li><a class=""  href="psyvariar-2-ultimate-final.html">Psyvariar 2 - Ultimate Final</a></li>
</ul>
</li>
<li>Mobile<ul>
<li><a class=""  href="azur-lane.html">Azur Lane</a></li>
</ul>
</li>
<li>Tabletop<ul>
<li><a class=""  href="mahjong/mahjong.html">Mahjong</a></li>
</ul>
</li>
</ul>
 ]]></content>
                </entry><entry>
                  <id>https://pfy.ch/music/music.html</id>
                  <title>Music</title>
                  <published>2024-08-08T05:54:00.000Z</published>
                  <updated>2025-04-30T08:55:01.000Z</updated>
                  <link>https://pfy.ch/music/music.html</link>
                  <emoji>🎵</emoji>
                  <content type="html" xml:base="https://pfy.ch/music/music.html"><![CDATA[ <p>I used to rotate through heaps of albums weekly when I was super into discovering new things. Nowadays, I&#39;ll usually pick a genre and stick to it for a while before trying out something else.</p>
<ul>
<li><a class=""  href="#recommendations">Recommendations</a></li>
<li><a class=""  href="#last-30-days">Last 30 Days</a></li>
</ul>
<h2 id="recommendations">Recommendations</h2>
<p>Albums that I&#39;ve listened too and really like. Worth looking them up on your preferred music service! I&#39;ll be adding more albums as I go. Potentially having them link to a more detailed page with more information. The list is randomised so it&#39;s not in order of preference.</p>
<div class="noScript">
    <p>Nothing will render here when Javascript is disabled since I'm using <a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements" target="_blank">custom HTML elements</a>.</p>
</div>

<div class="recommendations">
    <album-artwork name="Loveless" artist="My Bloody Valentine" artwork="https://d1rgjmn2wmqeif.cloudfront.net/r/b/223063-1.jpg"></album-artwork>
    <album-artwork name="Sunbather" artist="Deafheaven" artwork="https://f4.bcbits.com/img/a3169282583_16.jpg"></album-artwork>
    <album-artwork name="PetroDragonic Apocalypse" artist="King Gizzard & The Lizard Wizard" artwork="https://f4.bcbits.com/img/a2805471381_16.jpg"></album-artwork>
    <album-artwork name="Tourist" artist="St Germain" artwork="https://upload.wikimedia.org/wikipedia/en/8/86/St_Germain_-_Tourist.jpg"></album-artwork>
    <album-artwork name="WLFGRL" artist="Machine Girl" artwork="https://f4.bcbits.com/img/a1638146004_16.jpg"></album-artwork>
    <album-artwork name="My Wife is Drink Paint" artist="Bye2" artwork="https://f4.bcbits.com/img/a2870216307_16.jpg"></album-artwork>
    <album-artwork name="The Kaizo Manifesto" artist="Kaizo Slumber" artwork="https://f4.bcbits.com/img/a2323410766_16.jpg"></album-artwork>
    <album-artwork name="Offered Schematics Suggesting Peace" artist="Sound Tribe Sector 9" artwork="https://f4.bcbits.com/img/a2185945628_16.jpg"></album-artwork>
    <album-artwork name="Midtown 120 Blues" artist="DJ Sprinkles" artwork="https://lastfm.freetls.fastly.net/i/u/500x500/2b47dcb38285050ad28a42b1512cfe19.jpg"></album-artwork>
    <album-artwork name="Crystallize" artist="Tokyo Shoegazer" artwork="https://f4.bcbits.com/img/a0011466745_16.jpg"></album-artwork>
    <album-artwork name="Prince of the Cyber Rave" artist="Golden Boy" artwork="https://f4.bcbits.com/img/a1081101293_10.jpg"></album-artwork>
    <album-artwork name="American Football" artist="American Football" artwork="https://f4.bcbits.com/img/a3216079630_16.jpg"></album-artwork>
    <album-artwork name="Nescesary Discrepancy" artist="Arc System Works" artwork="https://medium-media.vgm.io/albums/75/118857/118857-a56bce185e32.jpg"></album-artwork>
    <album-artwork name="The Rise and Fall of a Midwest Princess" artist="Chappell Roan" artwork="https://upload.wikimedia.org/wikipedia/en/3/34/Chappell_Roan_-_The_Rise_and_Fall_of_a_Midwest_Princess.png"></album-artwork>
    <album-artwork name="Quarantine" artist="Laurel Halo" artwork="https://f4.bcbits.com/img/a2439830317_10.jpg"></album-artwork>
</div>

<h2 id="last-30-days">Last 30 Days</h2>
<p>If the API isn&#39;t down - below should be the last few albums I&#39;ve listened to over the last 30 days. I&#39;ve been using an <a class=""  href="./ipod.html">iPod Classic</a> lately though so this might not be <em>actually</em> what I&#39;ve listened to lately. The Last.fm API also kinda sucks now &amp; sometimes returns no data at all - This may be why artwork/titles may be missing!</p>
<div class="noScript">
    <p>Nothing will render here when Javascript is disabled since I'm using <a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements" target="_blank">custom HTML elements</a>.</p>
</div>

<p><recent-listens></recent-listens></p>
 ]]></content>
                </entry><entry>
                  <id>https://pfy.ch/thoughts/expenses.html</id>
                  <title>Planning Expenses</title>
                  <published>2024-08-06T08:27:26.472Z</published>
                  <updated>2024-11-25T07:23:09.000Z</updated>
                  <link>https://pfy.ch/thoughts/expenses.html</link>
                  <emoji>🤔</emoji>
                  <content type="html" xml:base="https://pfy.ch/thoughts/expenses.html"><![CDATA[ <p>My partner and I recently made quite a large move into a new unit in Sydney. Our rent went up in Wollongong by quite a bit, and we couldn&#39;t justify paying that much to live in <em>wollongong</em>. </p>
<p>We&#39;re both earning a pretty ok salary, but moving into literally one of the most expensive cities in the world can be a bit daunting. We wanted to try to visualise our expenses and see if it was even something we could afford.</p>
<p>I&#39;d tried financial planning before, but it always ends up being a chore, falling out of sync or just becoming way too much of a hassle. Instead, this time I tried to simplify things.</p>
<h2 id="plan-for-the-worst-case-scenario">Plan for the worst case scenario</h2>
<p>The first step of this plan is to write down <em>every</em> recurring expense. For variable things like groceries - pick the most expensive reasonable shop you did recently and round it up. Usually our weekly groceries range between $140 - $160 but depending on if we want snacks, are having guests or just want to buy some drinks it can sometimes be more. I&#39;ve rounded up Groceries to $200 in our planner. </p>
<p>I then split them into a Google sheet like so:</p>
<table>
<thead>
<tr>
<th>Expense</th>
<th>Yearly</th>
<th>Monthly</th>
<th>Fortnightly</th>
<th>Weekly</th>
</tr>
</thead>
<tbody><tr>
<td>Rent</td>
<td></td>
<td></td>
<td></td>
<td>$850</td>
</tr>
<tr>
<td>Groceries</td>
<td></td>
<td></td>
<td></td>
<td>$200</td>
</tr>
<tr>
<td>Utilities</td>
<td></td>
<td>$200</td>
<td></td>
<td></td>
</tr>
<tr>
<td>Car Rego</td>
<td>$1,500</td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>Youtube Premium</td>
<td></td>
<td>$11</td>
<td></td>
<td></td>
</tr>
</tbody></table>
<p>I then sum everything down into the weekly column, again assuming worst case:</p>
<ul>
<li>Monthly = total / 4<sup><a id="footnote-ref-1" href="#footnote-1" data-footnote-ref aria-describedby="footnote-label">1</a></sup></li>
<li>Yearly = total / 12</li>
</ul>
<table>
<thead>
<tr>
<th>Expense</th>
<th>Yearly</th>
<th>Monthly</th>
<th>Fortnightly</th>
<th>Weekly</th>
</tr>
</thead>
<tbody><tr>
<td>Rent</td>
<td></td>
<td></td>
<td></td>
<td>$850</td>
</tr>
<tr>
<td>Groceries</td>
<td></td>
<td></td>
<td></td>
<td>$200</td>
</tr>
<tr>
<td>Utilities</td>
<td></td>
<td>$200</td>
<td></td>
<td>$50</td>
</tr>
<tr>
<td>Car Rego</td>
<td>$1,500</td>
<td></td>
<td></td>
<td>$125</td>
</tr>
<tr>
<td>Youtube Premium</td>
<td></td>
<td>$11</td>
<td></td>
<td>$2.75</td>
</tr>
</tbody></table>
<p>Next calculate the totals:</p>
<ul>
<li>Yearly = weekly * 52</li>
<li>Monthly = yearly / 12</li>
<li>Fortnightly = weekly * 2</li>
</ul>
<table>
<thead>
<tr>
<th>Expense</th>
<th>Yearly</th>
<th>Monthly</th>
<th>Fortnightly</th>
<th>Weekly</th>
</tr>
</thead>
<tbody><tr>
<td>Rent</td>
<td></td>
<td></td>
<td></td>
<td>$850</td>
</tr>
<tr>
<td>Groceries</td>
<td></td>
<td></td>
<td></td>
<td>$200</td>
</tr>
<tr>
<td>Utilities</td>
<td></td>
<td>$200</td>
<td></td>
<td>$50</td>
</tr>
<tr>
<td>Car Rego</td>
<td>$1,500</td>
<td></td>
<td></td>
<td>$125</td>
</tr>
<tr>
<td>Youtube Premium</td>
<td></td>
<td>$11</td>
<td></td>
<td>$2.75</td>
</tr>
<tr>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td><strong>Total</strong></td>
<td>$63,843</td>
<td>$5,320.25</td>
<td>$2,455.50</td>
<td>$1,227.75</td>
</tr>
</tbody></table>
<p>These bills are then split between both my partner and I. We then each have our own table in the same spreadsheet with our own personal expenses. If there&#39;s a personal subscription in USD, assume the worst and select the highest recent expense and round it to the nearest dollar. For example, I&#39;m charged for my IDE in USD. It&#39;s 49 USD, and I was last charged $75.37, I round this up to $80. Assuming the worst case.</p>
<table>
<thead>
<tr>
<th>Expense</th>
<th>Yearly</th>
<th>Monthly</th>
<th>Fortnightly</th>
<th>Weekly</th>
</tr>
</thead>
<tbody><tr>
<td>Bills</td>
<td></td>
<td></td>
<td></td>
<td>$613.87</td>
</tr>
<tr>
<td>Domains</td>
<td>$150</td>
<td></td>
<td></td>
<td>$12.50</td>
</tr>
<tr>
<td>IDE</td>
<td>$80</td>
<td></td>
<td></td>
<td>$6.67</td>
</tr>
<tr>
<td>FFXIV</td>
<td></td>
<td>$20</td>
<td></td>
<td>$5</td>
</tr>
<tr>
<td>BackBlaze</td>
<td></td>
<td>$27</td>
<td></td>
<td>$6.75</td>
</tr>
<tr>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td><strong>Total</strong></td>
<td>$33,529.08</td>
<td>$2,794.09</td>
<td>$1,289.58</td>
<td>$644.79</td>
</tr>
</tbody></table>
<p>I get paid on a fortnightly basis - so from this table I know that I need to move at least $1,289.58 into my billing account each fortnight. This method also slowly builds up padding - not all expenses will likely come out at once, but if they do technically you will be able to handle them.</p>
<p>If you want as well you can add in an extra row showing the total minus your salary, giving you a good idea of how much money is left over you can save. </p>
<p>I&#39;ve left these examples extremely terse, but you should really add <em>everything</em> in.</p>
<ul>
<li>Go to the arcade a few times a week? Add $30 worth of &quot;coins&quot; to your expenses.</li>
<li>Sometimes order food out when your cant be arsed to cook? Add $50 for &quot;Uber eats&quot; to your expenses.</li>
<li>Have a random $3 recurring donation? Add that $3 to your spreadsheet!</li>
</ul>
<p>As I mentioned in my <a class=""  href="./calendars.html">calendar</a> post, what I really care about is seeing what&#39;s going at a glance - I don&#39;t really care how much is in my account now all I really care about is <em>&quot;can I afford to continue living how I currently am?&quot;</em>.</p>
<p>This method of planning might be naive, but it really helps me with the stresses that come with finance, it can help me plan for future purchases or savings goals and I know what sort of buffers I could have in the future. Obviously I pay attention to my bank balance and put away money to save long term but this does a really good job at doing what I need doing.</p>
<p>Unrelated but I&#39;d love if someone could email me a recommendation for a self-hosted version of Google sheets - I just want a good web spreadsheet program, even a nice cross-platform one with sharing would be amazing<sup><a id="footnote-ref-2" href="#footnote-2" data-footnote-ref aria-describedby="footnote-label">2</a></sup>.</p>
<section class="footnotes" data-footnotes>
<h2 id="footnote-label" class="sr-only">Footnotes</h2>
<ol>
<li id="footnote-1">
<p>4.35 is more accurate but 4 works better for this <a href="#footnote-ref-1" data-footnote-backref aria-label="Back to reference 1">↩</a></p>
</li>
<li id="footnote-2">
<p><strong>Update 2024/11/25:</strong> I&#39;ve started using ONLYOFFICE + NextCloud on my Home-server to make &amp; manage my documents. <a href="#footnote-ref-2" data-footnote-backref aria-label="Back to reference 2">↩</a></p>
</li>
</ol>
</section>
 ]]></content>
                </entry><entry>
                  <id>https://pfy.ch/thoughts/calendars.html</id>
                  <title>Please, use your calendar</title>
                  <published>2024-08-05T22:40:58.777Z</published>
                  <updated>2024-08-05T22:44:55.000Z</updated>
                  <link>https://pfy.ch/thoughts/calendars.html</link>
                  <emoji>🤔</emoji>
                  <content type="html" xml:base="https://pfy.ch/thoughts/calendars.html"><![CDATA[ <p>Literally every phone has a built-in calendar app - your email provider likely hosts one too.</p>
<p>Please use it!</p>
<p>The amount of times I&#39;ve gone to plan something with someone, and they&#39;ve agreed to hang out only to cancel last minute because &quot;I forgot it was my grandmas anniversary and my parents are hosting dinner&quot; or something crazy like that. </p>
<p>Calendar events <em>don&#39;t need</em> to be super accurate or detailed - they just need to be <em>there</em>. At a quick glance you can see if <strong>something</strong> is planned for that day.</p>
<p>Apple Calendar is great, Google calendar is great - Apple calendar even lets you add estimated travel time for driving or public transport on the event, so you can better estimate when to leave. I&#39;m not always on time for everything but being able to quickly visualise all this information in an app that&#39;s <em>literally preinstalled on most phones</em> is insane and more people need to take advantage of it.</p>
<h2 id="examples">Examples</h2>
<blockquote>
<p>&quot;Your appointment is at 3:15pm on the Thursday, is that ok for you?&quot;<br>&quot;That works for me&quot;<br>&quot;Do you need me to write it on a card for you?&quot;<br>&quot;No that&#39;s ok, thank you&quot;</p>
</blockquote>
<p>Put explicit appointments in your calendar <em>as soon as</em> you&#39;re told when they are. It&#39;ll save the hassle of having to be given a note, follow-up texts, emails, etc.</p>
<p>It&#39;s scary how often I&#39;ve seen people in doctor&#39;s offices ask for the time on a note because, &quot;I&#39;m so forgetful!&quot;. Please... use your calendar...</p>
<blockquote>
<p>&quot;Hey are you free for dinner at Rigo&#39;s on the 19th?&quot;<br>&quot;Yeah&quot;</p>
</blockquote>
<p>You didn&#39;t get an explicit time - the plan is probably still a bit up in the air. Create an event around 7pm (or whenever you feel is kinda accurate) and add travel time from wherever you think you&#39;ll be to the events&#39; location.</p>
<p>As the event gets closer try to follow up with more details and slowly update your event to be more accurate. Having an event there at least lets you know you&#39;ve got <em>something</em> on in the afternoon.</p>
<p>Now if someone asks you &quot;Hey do you want to come watch a movie on the 19th?&quot; you can decline knowing that it&#39;ll probably overlap with the dinner plans - or, you can try to solidify the plans to slot the movie in. All depends on how much effort you want to try to put in.</p>
<h2 id="wrapping-up">Wrapping up</h2>
<p>Over-estimate, over-plan, always make events worst-case-scenario long. You don&#39;t want to stretch yourself thin and end up being that person who&#39;s planned every minute of your day down. Keep it simple, round to the hour, whatever works best for you.</p>
<p>It makes shit <em>so</em> much easier to know what&#39;s up during your week, when you can just relax because you <strong>know</strong> nothing&#39;s on that day. It&#39;s great. I haven&#39;t even gotten into how insane it is to share a calendar with your partner or friends for common events - literally everyone with an email account has a calendar.</p>
 ]]></content>
                </entry><entry>
                  <id>https://pfy.ch/programming/4k-sunshine.html</id>
                  <title>4K Sunshine Streaming from a non-4k host</title>
                  <published>2024-07-02T11:50:36.925Z</published>
                  <updated>2024-07-02T12:02:03.000Z</updated>
                  <link>https://pfy.ch/programming/4k-sunshine.html</link>
                  <emoji>💻</emoji>
                  <content type="html" xml:base="https://pfy.ch/programming/4k-sunshine.html"><![CDATA[ <div class="banner">

<p>This guide assumes you meet the following criteria:</p>
<ul>
<li>You have a non-4K display on your Host but want to stream to a 4K TV</li>
<li>You don&#39;t want to use a Dummy HDMI Dongle</li>
<li>You are running Arch Linux or a derivative</li>
<li><a class="externalLink" target="_blank" href="https://github.com/LizardByte/Sunshine">Sunshine</a> is configured</li>
<li>You have a free output port on your GPU</li>
</ul>
</div>

<h2 id="contents">Contents</h2>
<ul>
<li><a class=""  href="#preamble">Preamble</a></li>
<li><a class=""  href="#getting-started">Getting started</a></li>
<li><a class=""  href="#getting-an-edid-file">Getting an EDID file</a></li>
<li><a class=""  href="#modifying-kernel-parameters">Modifying Kernel parameters</a></li>
<li><a class=""  href="#troubleshooting">Troubleshooting</a><ul>
<li><a class=""  href="#moving-the-virtual-screen">Moving the virtual screen</a></li>
<li><a class=""  href="#showing-the-virtual-screen-in-sunshine">Showing the virtual screen in Sunshine</a></li>
<li><a class=""  href="#moving-windows-to-the-virtual-screen">Moving windows to the virtual screen</a></li>
</ul>
</li>
</ul>
<h2 id="preamble">Preamble</h2>
<p>I mostly play games from my couch now, and I&#39;ve also mostly been playing Playstation 5. But there are some games that I really want to play on my PC - but on my couch. My SteamDeck docked has been good, but it&#39;s not as beefy as I&#39;d like - especially at 4k. I tried SteamLink for a while but the stream quality was serviceable at best.</p>
<p>I recently purchased Forza Horizon 4 on Steam and I really, <em>really</em> wanted to play it on the best settings at 4K but my SteamDeck <em>could not handle that</em> and SteamLink looked really average at 1080p. So I thought I&#39;d try <a class="externalLink" target="_blank" href="https://github.com/LizardByte/Sunshine">Sunshine</a> and see if I could get it to work - I&#39;d heard it looked much better than SteamLink and was a bit more flexible with settings.</p>
<p> After some tinkering I can confirm that Sunshine looks much nicer streaming from my PC to my SteamDeck plugged into my 4K TV. But now I&#39;m wondering: &quot;Can I stream 4k 120hz + HDR to my TV?&quot;. I only have a 1080p display plugged into my PC, so I had no idea how to get a 4K output captureable by Sunshine. A lot of posts online lead to dead ends and I tried messing with <code>xrandr</code> to make new modes for my existing monitors, but I couldn&#39;t get it to work.</p>
<p>I then finally found <a class="externalLink" target="_blank" href="https://old.reddit.com/r/linux_gaming/comments/199ylqz/streaming_with_sunshine_from_virtual_screens/">this Reddit post</a> with only 14 upvotes - after some tinkering It actually worked!</p>
<h2 id="getting-started">Getting started</h2>
<p>To get this working you must have an un-used port on your GPU.</p>
<p>The following command should output the name of unused ports on your GPU:</p>
<pre><code class="hljs language-bash"><span class="hljs-keyword">for</span> p <span class="hljs-keyword">in</span> /sys/class/drm/*/status; <span class="hljs-keyword">do</span> 
  con=<span class="hljs-variable">${p%/status}</span>; 
  <span class="hljs-built_in">echo</span> -n <span class="hljs-string">&quot;<span class="hljs-variable">${con#*/card?-}</span>: &quot;</span>; 
  <span class="hljs-built_in">cat</span> <span class="hljs-variable">$p</span>; 
<span class="hljs-keyword">done</span>
</code></pre><p>You should see something like this:</p>
<pre><code class="hljs">DP-1: connected
DP-2: disconnected
HDMI-A-1: disconnected
HDMI-A-2: connected
</code></pre><p>Take note of one of the ports that is <code>disconnected</code>. In my case it was <code>HDMI-A-1</code>.</p>
<h2 id="getting-an-edid-file">Getting an EDID file</h2>
<p>You&#39;ll then need an EDID file for the monitor you&#39;re streaming to (or one similar). I used the <code>samsung-q800t-hdmi2.1</code> (the same as OP) which I got from <a class="externalLink" target="_blank" href="https://git.linuxtv.org/edid-decode.git/tree/data">here</a> since it supports the features I need and that are supported by my output TV.</p>
<p>Once this EDID file is downloaded you&#39;ll need to move it into <code>/usr/lib/firmware/edid</code>.</p>
<pre><code class="hljs language-bash"><span class="hljs-built_in">sudo</span> <span class="hljs-built_in">mkdir</span> -p /usr/lib/firmware/edid <span class="hljs-comment"># Create the directory if it doesn&#x27;t exist</span>
<span class="hljs-built_in">sudo</span> <span class="hljs-built_in">mv</span> samsung-q800t-hdmi2.1 /usr/lib/firmware/edid
</code></pre><h2 id="modifying-kernel-parameters">Modifying Kernel parameters</h2>
<p>Next we&#39;ll need to include this EDID file in our <code>/etc/mkinitcpio.conf</code> file <code>FILES</code> array so that it&#39;s loaded at boot.</p>
<pre><code class="hljs language-bash">FILES=(<span class="hljs-string">&quot;/usr/lib/firmware/edid/samsung-q800t-hdmi2.1&quot;</span>)
</code></pre><p>and finally we&#39;ll modify <code>/boot/grub/grub.cfg</code> to load this EDID file and apply to the unused output with the following kernel parameters:</p>
<pre><code class="hljs language-bash">drm.edid_firmware=HDMI-A-1:edid/samsung-q800t-hdmi2.1 video=HDMI-A-1:e
</code></pre><p>Your <code>grub.cfg</code> should now look something like this (I&#39;ve removed entries and other parameters for brevity):</p>
<pre><code class="hljs language-bash">menuentry <span class="hljs-string">&#x27;Distro, on linux&#x27;</span> --class distro --class gnu-linux --class gnu --class os <span class="hljs-variable">$menuentry_id_option</span> <span class="hljs-string">&#x27;gnulinux-linux-advanced&#x27;</span> {
        [...] <span class="hljs-comment"># Truncated for brevity</span>
        linux /boot/vmlinuz-linux drm.edid_firmware=HDMI-A-1:edid/samsung-q800t-hdmi2.1 video=HDMI-A-1:e <span class="hljs-comment"># Append them here </span>
        <span class="hljs-built_in">echo</span>    <span class="hljs-string">&#x27;Loading initial ramdisk ...&#x27;</span>
        initrd  /boot/amd-ucode.img /boot/initramfs-linux.img
}
</code></pre><p>If you&#39;re not using grub you&#39;ll need to add the kernel parameters <a class="externalLink" target="_blank" href="https://wiki.archlinux.org/title/Kernel_parameters">some other way</a>.</p>
<p>Once you reboot your system you should now have an additional output appearing in <code>xrandr</code>/<code>sunshine</code> reporting that it&#39;s a 4K display.</p>
<h2 id="troubleshooting">Troubleshooting</h2>
<h3 id="moving-the-virtual-screen">Moving the virtual screen</h3>
<p>You may need to run the following commands if it doesn&#39;t appear at first or is not where you expect it to be:</p>
<pre><code class="hljs language-bash">xrandr --output HDMI-A-1 --mode 3840x2160_120.00 --right-of DisplayPort-0 <span class="hljs-comment"># or wherever you want it to live </span>
</code></pre><p>The monitor may also appear in your DEs display layout settings.</p>
<h4 id="showing-the-virtual-screen-in-sunshine">Showing the virtual screen in Sunshine</h4>
<p>For the monitor to appear in Sunshine&#39;s startup logs you&#39;ll need to navigate to Configuration &gt; Advanced and then set &quot;Force a specific Capture Method&quot; to &quot;KMS&quot;. If the capture method is <em>not set to KMS</em>, Sunshine will not show or be able to access the virtual screen.</p>
<p>You should then see the fake Samsung display in the command output when restarting Sunshine:</p>
<pre><code class="hljs language-bash">[2024:07:02:21:40:12]: Info: -------- Start of KMS monitor list --------
[2024:07:02:21:40:12]: Info: Monitor 2 is DP-1: AOC 24G1WG4/0x00075229
[2024:07:02:21:40:12]: Info: Monitor 1 is HDMI-A-1: Samsung Electric Company SAMSUNG/0x01000E00
[2024:07:02:21:40:12]: Info: Monitor 0 is HDMI-A-2: AOC 2460G5/GKAH4HA014907
</code></pre><p>Set the correct monitor in sunshine at Configuration &gt; Audio/Video and restart Sunshine.</p>
<h3 id="moving-windows-to-the-virtual-screen">Moving windows to the virtual screen</h3>
<p>A commenter in the thread <a class="externalLink" target="_blank" href="https://gist.github.com/MrHighVoltage/78ca58218a569d253433fd4be883c6c3">shared the following scripts</a> to assist in actually using the virtual screen but for now I&#39;m using the Super + Arrow Keys shortcut, or Super + Drag action in KDE to move windows to the virtual screen. I&#39;ll create a new post if I ever do get around to setting this up properly.</p>
 ]]></content>
                </entry><entry>
                  <id>https://pfy.ch/programming/homebridge-via-moonraker.html</id>
                  <title>Controlling a HomeBridge accessory via Moonraker</title>
                  <published>2024-06-27T08:37:10.911Z</published>
                  <updated>2024-06-27T08:59:39.000Z</updated>
                  <link>https://pfy.ch/programming/homebridge-via-moonraker.html</link>
                  <emoji>💻</emoji>
                  <content type="html" xml:base="https://pfy.ch/programming/homebridge-via-moonraker.html"><![CDATA[ <p>I run a HomeBridge server on my local network to control various accessories around the house. Eventually I do plan on swapping to HomeBridge + HomeAssistant but for now - by itself - I&#39;m happy with the functionality I have.</p>
<p>I recently upgraded my Ender3 to run Klipper firmware and use Fluidd to control my 3D printer. I used to run a plugin in Octoprint to control the power board the printer is connected to but no such plugin exists for Klipper.</p>
<p>Moonraker offers power management, however HomeBridge is not offered by default and requires setting up a generic HTTP device.</p>
<p>HomeBridge offers a basic REST API for interacting with accessories when HomeBridge is running in Insecure Mode (Started with <code>homebridge -I</code>).</p>
<h2 id="getting-aid-iid-from-homebridge">Getting aid & iid from HomeBridge</h2>
<p>This is a little bit of a hassle since the aid &amp; iid reported in the HomeBridge UI are sometimes not actually the ones you need to use.</p>
<p>Hitting <code>GET http://xxx.xxx.xxx.xxx:52626/accessories</code> will return a list of all the accessories in your network. Include the <code>Authorization</code> header with 8-digit number on HomeBridges main screen as the value.</p>
<pre><code class="hljs language-http">&gt; GET /accessories HTTP/1.1
&gt; Host: 127.0.0.1:52626
&gt; User-Agent: insomnia/2023.5.8
&gt; authorization: 643-36-418
&gt; Accept: */*
</code></pre><p>Through poking around in here I found my 3D printer&#39;s aid &amp; iid. In the HomeBridge UI it reports as <code>aid: 3</code> and <code>iid: 9</code> but I found that the actual iid was different - <code>iid: 10</code>.</p>
<p>You can hit the URL <code>GET http://xxx.xxx.xxx.xxx:52626/characteristics?id=3.10</code> (with your actual aid &amp; iid) to get the characteristics for your accessory. It should look something like this, with value representing on/off:</p>
<pre><code class="hljs language-http">&gt; GET /characteristics?id=3.10 HTTP/1.1
&gt; Host: 127.0.0.1:52626
&gt; User-Agent: insomnia/2023.5.8
&gt; authorization: 643-36-418
&gt; Accept: */*
</code></pre><pre><code class="hljs language-json"><span class="hljs-punctuation">{</span>
	<span class="hljs-attr">&quot;characteristics&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span>
		<span class="hljs-punctuation">{</span>
			<span class="hljs-attr">&quot;aid&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">3</span><span class="hljs-punctuation">,</span>
			<span class="hljs-attr">&quot;iid&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">10</span><span class="hljs-punctuation">,</span>
			<span class="hljs-attr">&quot;value&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">1</span>
		<span class="hljs-punctuation">}</span>
	<span class="hljs-punctuation">]</span>
<span class="hljs-punctuation">}</span>
</code></pre><p>Finally, you can test setting this value by hitting <code>PUT http://xxx.xxx.xxx.xxx:52626/characteristics</code> </p>
<pre><code class="hljs language-http">&gt; PUT /characteristics HTTP/1.1
&gt; Host: 127.0.0.1:52626
&gt; User-Agent: insomnia/2023.5.8
&gt; Content-Type: Application/json
&gt; authorization: 643-36-418
&gt; Accept: */*
&gt; Content-Length: 50

| {&quot;characteristics&quot;:[{&quot;aid&quot;:3,&quot;iid&quot;:10,&quot;value&quot;:1}]}
</code></pre><h2 id="moonraker-power-configuration">Moonraker power configuration</h2>
<p>The following is the moonraker configuration for controlling the HomeBridge accessory.</p>
<p>Ensure you: </p>
<ul>
<li>update <code>on_url</code>, <code>off_url</code> &amp; <code>status_url</code> with your HomeBridge IP.</li>
<li>update <code>aid</code> &amp; <code>iid</code> with your actual aid &amp; iid in <code>status_url</code> (e.g. <code>?id=3.10</code>)</li>
<li>update <code>authorization</code> on line one of <code>request_template</code> with your actual authorization code.</li>
<li>update the payloads in <code>request_template</code> with your actual aid &amp; iid on lines <code>4</code> &amp; <code>9</code>.</li>
</ul>
<pre><code class="hljs language-yaml">[<span class="hljs-string">power</span> <span class="hljs-string">printer</span>]
<span class="hljs-attr">type:</span> <span class="hljs-string">http</span>
<span class="hljs-attr">on_url:</span> <span class="hljs-string">http://xxx.xxx.xxx.xxx:52626/characteristics</span>
<span class="hljs-attr">off_url:</span> <span class="hljs-string">http://xxx.xxx.xxx.xxx:52626/characteristics</span>
<span class="hljs-attr">status_url:</span> <span class="hljs-string">http://xxx.xxx.xxx.xxx:52626/characteristics?id=&lt;aid&gt;.&lt;iid&gt;</span>
<span class="hljs-attr">locked_while_printing:</span> <span class="hljs-literal">true</span>
<span class="hljs-attr">off_when_shutdown:</span> <span class="hljs-literal">true</span>
<span class="hljs-attr">request_template:</span>
    {<span class="hljs-string">%</span> <span class="hljs-string">do</span> <span class="hljs-string">http_request.add_header(&quot;authorization&quot;</span>, <span class="hljs-string">&quot;XXX-XX-XXX&quot;</span><span class="hljs-string">)</span> <span class="hljs-string">%</span>}
    {<span class="hljs-string">%</span> <span class="hljs-string">if</span> <span class="hljs-string">command</span> <span class="hljs-string">==</span> <span class="hljs-string">&quot;on&quot;</span> <span class="hljs-string">%</span>}
        {<span class="hljs-string">log_debug(&quot;Turning</span> <span class="hljs-string">printer</span> <span class="hljs-string">on&quot;)</span>}
        {<span class="hljs-string">%</span> <span class="hljs-string">do</span> <span class="hljs-string">http_request.set_body(</span>{<span class="hljs-string">&quot;characteristics&quot;</span><span class="hljs-string">:</span>[{<span class="hljs-string">&quot;aid&quot;</span><span class="hljs-string">:&lt;aid&gt;</span>,<span class="hljs-string">&quot;iid&quot;</span><span class="hljs-string">:&lt;iid&gt;</span>,<span class="hljs-string">&quot;value&quot;</span><span class="hljs-string">:1</span>}]}<span class="hljs-string">)</span> <span class="hljs-string">%</span>}
        {<span class="hljs-string">%</span> <span class="hljs-string">do</span> <span class="hljs-string">http_request.set_method(&quot;PUT&quot;)</span> <span class="hljs-string">%</span>}
        {<span class="hljs-string">%</span> <span class="hljs-string">do</span> <span class="hljs-string">http_request.send()</span> <span class="hljs-string">%</span>}
    {<span class="hljs-string">%</span> <span class="hljs-string">elif</span> <span class="hljs-string">command</span> <span class="hljs-string">==</span> <span class="hljs-string">&quot;off&quot;</span> <span class="hljs-string">%</span>}
        {<span class="hljs-string">log_debug(&quot;Turning</span> <span class="hljs-string">printer</span> <span class="hljs-string">off&quot;)</span>}
        {<span class="hljs-string">%</span> <span class="hljs-string">do</span> <span class="hljs-string">http_request.set_body(</span>{<span class="hljs-string">&quot;characteristics&quot;</span><span class="hljs-string">:</span>[{<span class="hljs-string">&quot;aid&quot;</span><span class="hljs-string">:&lt;aid&gt;</span>,<span class="hljs-string">&quot;iid&quot;</span><span class="hljs-string">:&lt;iid&gt;</span>,<span class="hljs-string">&quot;value&quot;</span><span class="hljs-string">:0</span>}]}<span class="hljs-string">)</span> <span class="hljs-string">%</span>}
        {<span class="hljs-string">%</span> <span class="hljs-string">do</span> <span class="hljs-string">http_request.set_method(&quot;PUT&quot;)</span> <span class="hljs-string">%</span>}
        {<span class="hljs-string">%</span> <span class="hljs-string">do</span> <span class="hljs-string">http_request.send()</span> <span class="hljs-string">%</span>}
    {<span class="hljs-string">%</span> <span class="hljs-string">elif</span> <span class="hljs-string">command</span> <span class="hljs-string">==</span> <span class="hljs-string">&quot;status&quot;</span> <span class="hljs-string">%</span>}
        {<span class="hljs-string">log_debug(&quot;Getting</span> <span class="hljs-string">printer</span> <span class="hljs-string">status&quot;)</span>}
        {<span class="hljs-string">%</span> <span class="hljs-string">do</span> <span class="hljs-string">http_request.set_method(&quot;GET&quot;)</span> <span class="hljs-string">%</span>}
        {<span class="hljs-string">%</span> <span class="hljs-string">do</span> <span class="hljs-string">http_request.send()</span> <span class="hljs-string">%</span>}
    {<span class="hljs-string">%</span> <span class="hljs-string">endif</span> <span class="hljs-string">%</span>}
    
<span class="hljs-attr">response_template:</span>
    {<span class="hljs-string">%</span> <span class="hljs-string">if</span> <span class="hljs-string">command</span> <span class="hljs-string">in</span> [<span class="hljs-string">&quot;on&quot;</span>, <span class="hljs-string">&quot;off&quot;</span>] <span class="hljs-string">%</span>}
        {<span class="hljs-string">%</span> <span class="hljs-string">do</span> <span class="hljs-string">async_sleep(1.0)</span> <span class="hljs-string">%</span>}
        {<span class="hljs-string">%</span> <span class="hljs-string">do</span> <span class="hljs-string">http_request.set_method(&quot;GET&quot;)</span> <span class="hljs-string">%</span>}
        {<span class="hljs-string">%</span> <span class="hljs-string">do</span> <span class="hljs-string">http_request.set_body(None)</span> <span class="hljs-string">%</span>}
        {<span class="hljs-string">%</span> <span class="hljs-string">do</span> <span class="hljs-string">http_request.set_url(urls.status)</span> <span class="hljs-string">%</span>}
        {<span class="hljs-string">%</span> <span class="hljs-string">do</span> <span class="hljs-string">http_request.send()</span> <span class="hljs-string">%</span>}
    {<span class="hljs-string">%</span> <span class="hljs-string">endif</span> <span class="hljs-string">%</span>}

    {<span class="hljs-string">%</span> <span class="hljs-string">set</span> <span class="hljs-string">resp</span> <span class="hljs-string">=</span> <span class="hljs-string">http_request.last_response().json()</span> <span class="hljs-string">%</span>}    
    {<span class="hljs-string">%</span> <span class="hljs-string">set</span> <span class="hljs-string">val</span> <span class="hljs-string">=</span> <span class="hljs-string">resp.characteristics</span>[<span class="hljs-number">0</span>]<span class="hljs-string">.value</span> <span class="hljs-string">%</span>}
    {<span class="hljs-string">%</span> <span class="hljs-string">if</span> <span class="hljs-string">val</span> <span class="hljs-string">==</span> <span class="hljs-number">0</span> <span class="hljs-string">%</span>}
        <span class="hljs-string">off</span>
    {<span class="hljs-string">%</span> <span class="hljs-string">elif</span> <span class="hljs-string">val</span> <span class="hljs-string">==</span> <span class="hljs-number">1</span> <span class="hljs-string">%</span>}
        <span class="hljs-string">on</span>
    {<span class="hljs-string">%</span> <span class="hljs-string">else</span> <span class="hljs-string">%</span>}
        <span class="hljs-string">off</span> 
    {<span class="hljs-string">%</span> <span class="hljs-string">endif</span> <span class="hljs-string">%</span>}
</code></pre> ]]></content>
                </entry><entry>
                  <id>https://pfy.ch/programming/func-godot.html</id>
                  <title>Mapping with TrenchBroom and Func Godot</title>
                  <published>2024-06-02T00:35:49.897Z</published>
                  <updated>2024-06-04T23:17:40.000Z</updated>
                  <link>https://pfy.ch/programming/func-godot.html</link>
                  <emoji>💻</emoji>
                  <content type="html" xml:base="https://pfy.ch/programming/func-godot.html"><![CDATA[ <style>
img {
    max-width: unset !important;
    max-height: 500px;
}
</style>

<p>While setting up <code>func_godot</code> I found the documentation very overwhelming as a beginner - everything you need is there, but it&#39;s not laid out in a step by step easy to follow process.</p>
<p>The following is a personal guide that I made if I ever have to set up a project again.</p>
<h1 id="table-of-contents">Table of Contents</h1>
<ul>
<li><a class=""  href="#required-files">Required files</a></li>
<li><a class=""  href="#getting-started">Getting Started</a><ul>
<li><a class=""  href="#setting-up-godot">Setting up Godot</a></li>
<li><a class=""  href="#configuring-trenchbroom">Configuring TrenchBroom</a></li>
<li><a class=""  href="#compiling-the-map-in-godot">Compiling the map in Godot</a></li>
<li><a class=""  href="#adding-textures">Adding Textures</a></li>
<li><a class=""  href="#custom-entities">Custom Entities</a><ul>
<li><a class=""  href="#placing-scenes-via-trenchbroom">Placing Scenes via TrenchBroom</a></li>
<li><a class=""  href="#placing-nodes-via-trenchbroom">Placing Nodes via TrenchBroom</a></li>
</ul>
</li>
</ul>
</li>
</ul>
<h1 id="required-files">Required files</h1>
<p>The following resources are required:</p>
<ul>
<li><a class="externalLink" target="_blank" href="https://github.com/func-godot/func_godot_plugin">Release of Func Godot</a> <em>(2024.1.1 used)</em></li>
<li><a class="externalLink" target="_blank" href="https://github.com/func-godot/func_godot_plugin">TrenchBroom</a> <em>(2024.1 used)</em></li>
<li><a class="externalLink" target="_blank" href="https://godotengine.org/">Godot 4</a>  <em>(v4.2.2.stable.arch_linux used)</em></li>
</ul>
<pre><code class="hljs language-bash">yay -Syyu godot trenchbroom-bin
</code></pre><h1 id="getting-started">Getting Started</h1>
<h2 id="setting-up-godot">Setting up Godot</h2>
<p>Before setting up TrenchBroom you must first set up the plugin - since it will generate files required for TrenchBroom.</p>
<ol>
<li>Create a new Godot project and copy the <code>addons</code> folder from your Func Godot release into the project root </li>
<li>Navigate to &quot;Project&quot; &gt; &quot;Project Settings&quot; &gt; &quot;Plugins&quot; and enable the plugin</li>
<li>Create the folders <code>trenchbroom</code> &amp; <code>trenchbroom/textures</code> in your project root</li>
<li>Open <code>res://addons/func_godot/func_godot_local_config.tres</code> and set the following fields:<ul>
<li><strong>Fgd Output Folder</strong>: This is where the custom TrenchBroom game definition will live. Set the value to something like this:  <ul>
<li><code>/home/user/.TrenchBroom/games/demo</code> where &quot;user&quot; is your username and &quot;demo&quot; is the name of your project.</li>
</ul>
</li>
<li><strong>TrenchBroom Game Config Folder</strong>: This should be exactly the same as &quot;Fgd Output Folder&quot;</li>
<li><strong>Map Editor Game Path</strong>: Set this path to the &quot;TrenchBroom&quot; folder in your Godot Project:  <ul>
<li><code>/home/user/Developer/demo-godot/trenchbroom</code></li>
</ul>
</li>
</ul>
</li>
<li>Click &quot;Export Func Godot Settings&quot; - It looks like a toggle, but it&#39;s actually a button.</li>
</ol>
<p><img src="/guide-images/trenchbroom/func_godot_local_config.png" alt="" /></p>
<ol start="6">
<li>Right-click the <code>trenchbroom</code> folder in your project root and &quot;Create New&quot; &gt; &quot;Resource&quot;</li>
<li>Create a new <code>FuncGodotFGDFile</code> and save it in the trenchbroom folder as &quot;primary_fgd.tres&quot;</li>
<li>Set the &quot;Base FGD File&quot; value to <code>res://addons/func_godot/fgd/func_godot_fgd.tres</code></li>
</ol>
<p><img src="/guide-images/trenchbroom/func_godot_fgd_file.png" alt="" /></p>
<ol start="9">
<li>Open <code>res://addons/func_godot/func_godot_default_map_settings.tres</code> and set the following:<ul>
<li><strong>Entity Fgd</strong>: Set this to the FGD we created earlier  <ul>
<li>i.e. <code>res://trenchbroom/primary_fgd.tres</code></li>
</ul>
</li>
<li><strong>Base Texture Dir</strong>: Set this to your map texture directory.  <ul>
<li>i.e. <code>res://trenchbroom/textures</code> to keep map textures separate to game textures</li>
</ul>
</li>
</ul>
</li>
</ol>
<p><img src="/guide-images/trenchbroom/func_godot_default_map_settings.png" alt="" /></p>
<ol start="10">
<li>Next open <code>res://addons/func_godot/game_config/trenchbroom/func_godot_tb_game_config.tres</code> and set the following:<ul>
<li><strong>Game Name</strong>: The folder name you wrote in &quot;Fgd Output Folder&quot;  <ul>
<li>i.e. <code>Demo</code></li>
</ul>
</li>
</ul>
</li>
<li>Click &quot;Export File&quot;</li>
</ol>
<p><img src="/guide-images/trenchbroom/func_godot_tb_game_config.png" alt="" /></p>
<h2 id="configuring-trenchbroom">Configuring TrenchBroom</h2>
<ol>
<li>Open TrenchBroom and click &quot;New map...&quot;</li>
<li>Click &quot;Open preferences...&quot;</li>
<li>Your games name should be listed in the games list - select it</li>
<li>Set the game path to the <code>res://trenchbroom/</code> folder in your Godot project root<ul>
<li>i.e. <code>/home/user/Developer/demo/trenchbroom</code></li>
</ul>
</li>
<li>Click &quot;Apply&quot; the &quot;Ok&quot;</li>
<li>Select your game from the right hand list then click &quot;Ok&quot;</li>
<li>Save the newly created map to your Godot project<ul>
<li>i.e. <code>/home/user/Developer/demo/maps/</code></li>
</ul>
</li>
</ol>
<h2 id="compiling-the-map-in-godot">Compiling the map in Godot</h2>
<ol>
<li>Back in Godot create a new 3D scene and save it<ul>
<li>i.e. <code>res://scenes/main.tscn</code></li>
</ul>
</li>
<li>Add a <code>FuncGodotMap</code> node to this scene</li>
<li>Set the &quot;Local Map File&quot; to the map you just saved<ul>
<li>i.e. <code>res://maps/main.map</code></li>
</ul>
</li>
<li>Click Build in the toolbar - You should see the default TrenchBroom cube as a mesh in Godot</li>
</ol>
<p><img src="/guide-images/trenchbroom/FuncGodotMap.png" alt="" /></p>
<h2 id="adding-textures">Adding Textures</h2>
<p>I use a site like <a class="externalLink" target="_blank" href="https://ambientcg.com">AmbientCG</a> for quick textures - I&#39;ll be using <a class="externalLink" target="_blank" href="https://ambientcg.com/view?id=Bricks085">this brick texture</a> in 4K as an example </p>
<ol>
<li>Copy your texture jpg into <code>res://trenchbroom/textures</code><ul>
<li>If your texture comes with normals, ao, roughness, displacements etc do the following:<ol>
<li>Create a folder with the same name as the root texture<ul>
<li><code>res://trenchbroom/textures/bricks</code></li>
</ul>
</li>
<li>Copy the files into this folder with the following naming convention:<ul>
<li><code>bricks_ao.jpg</code> - Ambient occlusion</li>
<li><code>bricks_roughness.jpg</code> - Roughness</li>
<li><code>bricks_displacement.jpg</code> - Displacement</li>
<li><code>bricks_normal.jpg</code> - Normal maps</li>
</ul>
</li>
</ol>
</li>
</ul>
</li>
<li>In Godot select all your imported textures in the file browser</li>
<li>Click &quot;Import&quot; at the top &amp; ensure &quot;mode&quot; is &quot;VRAM uncompressed&quot;</li>
<li>Click &quot;Re-import&quot;</li>
</ol>
<p><img src="/guide-images/trenchbroom/textures.png" alt="" /></p>
<ol start="5">
<li>Inside TrenchBroom press F5 or &quot;File&quot; &gt; &quot;Reload Texture Collections&quot;</li>
<li>You should now be able to apply the texture to brushes in TrenchBroom<ul>
<li>With 4k textures - I usually set the scale to <code>0.05</code></li>
</ul>
</li>
</ol>
<p><img src="/guide-images/trenchbroom/trenchbroom-textures.png" alt="" /></p>
<ol start="7">
<li>Back in Godot - Select your <code>FuncGodotMap</code> node and click &quot;Build&quot; &amp; &quot;Unwrap UV2&quot;. You should now see your texture applied.<ul>
<li>You can confirm all your materials applied correctly by checking the generated <code>.tres</code> file in your textures folder<ul>
<li>i.e .<code>res://trenchbroom/textures/bricks.tres</code></li>
</ul>
</li>
</ul>
</li>
</ol>
<p><img src="/guide-images/trenchbroom/godot-textures.png" alt="" /></p>
<h2 id="custom-entities">Custom Entities</h2>
<p>Jumping over into a different project (configured the same way), you may want to place entities such as lights, enemies, the player, etc. in your map via TrenchBroom. This can be quite complex however I have figured out the following</p>
<h3 id="placing-scenes-via-trenchbroom">Placing Scenes via TrenchBroom</h3>
<ol>
<li>Create the folder <code>res://TrenchBroom/entities</code></li>
<li>Right-click and &quot;Create New&quot; &gt; &quot;Resource&quot;</li>
<li>Create a <code>FuncGodotFGDPointClass</code> and name it after your entity</li>
<li>Set the following values:<ul>
<li><strong>Scene</strong><ul>
<li><strong>Scene File</strong>: The scene you want to create<ul>
<li>i.e. <code>character.tscn</code></li>
</ul>
</li>
</ul>
</li>
<li><strong>Entity Definition</strong><ul>
<li><strong>Class Name</strong>: The name of the entity in TrenchBroom</li>
<li><strong>Description</strong>: The description of the entity in TrenchBroom</li>
<li><strong>Meta Properties</strong>: The size and colour of the entity in TrenchBroom</li>
</ul>
</li>
</ul>
</li>
</ol>
<p><img src="/guide-images/trenchbroom/player-entity.png" alt="" /></p>
<ol start="5">
<li>Open <code>res://TrenchBroom/primary_fgd.tres</code></li>
<li>Under &quot;Entity Definitions&quot; add your new <code>.tres</code> file</li>
<li>Click &quot;Export File&quot;</li>
<li>In TrenchBroom, ensure the entities mod is enabled in the &quot;Map&quot; tab and press F6 or &quot;File&quot; &gt; &quot;Reload Entity Definitions&quot; to reload entities</li>
</ol>
<p><img src="/guide-images/trenchbroom/trenchbroom-entities.png" alt="" /></p>
<ol start="9">
<li>Back in Godot re-build your map and your entity should be present</li>
</ol>
<p><img src="/guide-images/trenchbroom/entities-godot.png" alt="" /></p>
<h3 id="placing-nodes-via-trenchbroom">Placing nodes via TrenchBroom</h3>
<p>Placing nodes like lights via TrenchBroom is a bit more complicated (I havent figured it out). But it&#39;s 100% possible &amp; I&#39;ve seen it done in the Discord. Once I have done it myself I will document the process here.</p>
<p>There are examples in this <a class="externalLink" target="_blank" href="https://github.com/func-godot/func_godot_example_basic/tree/main">GitHub Repo</a> which seem to work. </p>
 ]]></content>
                </entry><entry>
                  <id>https://pfy.ch/photography/urbex/assorted/tokyo-2024.html</id>
                  <title>Tokyo 2024</title>
                  <published>2024-04-22T00:40:45.242Z</published>
                  <updated>2025-03-01T07:11:29.000Z</updated>
                  <link>https://pfy.ch/photography/urbex/assorted/tokyo-2024.html</link>
                  <emoji>✈️</emoji>
                  <content type="html" xml:base="https://pfy.ch/photography/urbex/assorted/tokyo-2024.html"><![CDATA[ <!-- Testing out some new styles -->
<style>
    p:has(img) {
        display: flex;
        flex-wrap: wrap;
    }
    
    p:has(img) img {
        flex: 1 1 50%;
        min-width: 300px;
    }
</style>

<p>This is the first bit of proper shooting I&#39;ve done with my <a class=""  href="../../cameras/fuji-xt30-ii.html">XT30-II</a>. The camera is <em>really</em> good, and I&#39;m consistently impressed with how nice images look straight out of the camera without any edits. I highly recommend it if you&#39;re looking for a camera to daily since it can be quite compact with the lenses available for it.</p>
<p><img src="https://assets.pfy.ch/md/DSCF0125.jpg" alt="A small grocer with goods spilling onto the street" />
<img src="https://assets.pfy.ch/md/DSCF0129.jpg" alt="Two canned coffees on a messy desk" /></p>
<p>This is now the second time I&#39;ve been overseas or on some-sort-of holiday. The vibes around Tokyo are very much different when traveling <em>with</em> someone else - the city felt much less lonely.</p>
<p>We did more touristy stuff and I genuinely enjoyed my time much more. I still met up with friends and still spent heaps of time in the arcade, but it was nice to have an extra person giving you a push to try new things.</p>
<p><img src="https://assets.pfy.ch/md/DSCF0133.jpg" alt="A silhouette of a person in front of an enclosure full of jellyfish" />
<img src="https://assets.pfy.ch/md/DSCF0136.jpg" alt="A pool full of glowing jellyfish" /></p>
<p>I definitely did not take as many photos as I would have liked to - I really wanted to focus on shooting only with my XT30. Last time I traveled I had my <a class=""  href="../../cameras/fuji-dl-300.html">DL-300</a> which was pocket-able - whereas this camera (even with a cookie lens) was just slightly too large to fit in my pocket.</p>
<p>Even though I didn&#39;t take as many photos I still am really happy with the pictures I did capture. We had an Instax Mini camera with us as well which took some really pretty polaroids which now live permanently on our fridge. Last year with the DL-300 I shot more since I didn&#39;t want to &#39;waste&#39; film I&#39;d specifically saved for the trip - even though I got <a class=""  href="tokyo-2023.html">heaps of amazing photos from that</a> there were still loads of stinkers that will never see the light of day. </p>
<p><img src="https://assets.pfy.ch/md/DSCF0155.jpg" alt="A grey day with bare trees, there is a roller-coaster in the background" />
<img src="https://assets.pfy.ch/md/DSCF0158.jpg" alt="An overhead view of a shopping park" />
<img src="https://assets.pfy.ch/md/DSCF0161.jpg" alt="An overhead view of a street" />
<img src="https://assets.pfy.ch/md/DSCF0163.jpg" alt="A polaroid type camera and two developing images sitting on a bench" /></p>
<p>We went during the Tokyo Marathon since it lined up with some other friends traveling - It was really nice to meet up and spend time with everyone. Even if it was in a Saizeriya with a baby crying at 90db in the booth next to us.</p>
<p>I think next time I travel it will probably be somewhere other than Tokyo, I still feel like there&#39;s more to do but doing it for a 3rd time in a row would feel like a bit of a cop-out. We&#39;ve had a friend offer us board in New York if we ever end up traveling there so maybe next year we might try to do Christmas in NYC?</p>
<p><img src="https://assets.pfy.ch/md/DSCF0168.jpg" alt="A tunnel with two train tracks opening over a river" />
<img src="https://assets.pfy.ch/md/DSCF0172.jpg" alt="Giant cup noodle" />
<img src="https://assets.pfy.ch/md/DSCF0178.jpg" alt="A giant animatronic robot inside scaffolding" />
<img src="https://assets.pfy.ch/md/DSCF0180.jpg" alt="A sunlit wooden stairway" /></p>
<p>We&#39;ll see.</p>
 ]]></content>
                </entry><entry>
                  <id>https://pfy.ch/photography/cameras/fuji-xt30-ii.html</id>
                  <title>FujiFilm XT30 II</title>
                  <published>2024-02-18T07:05:52.838Z</published>
                  <updated>2024-06-04T23:17:39.000Z</updated>
                  <link>https://pfy.ch/photography/cameras/fuji-xt30-ii.html</link>
                  <emoji>📷</emoji>
                  <content type="html" xml:base="https://pfy.ch/photography/cameras/fuji-xt30-ii.html"><![CDATA[ <p>A few months ago I purchased myself a FujiFilm XT30 II &amp; a 35mm F2 Lens. I wanted a nice compact camera I could use while traveling &amp; for street photography.</p>
<p><img src="https://assets.pfy.ch/md/DSCF0075.jpg" alt="" />
<img src="https://assets.pfy.ch/md/DSCF0003.jpg" alt="" /></p>
<p>The main reason for picking the XT30 II was the default output. With minimal tinkering or editing the images look great straight out of the camera. It fits pretty well in my backpack too and is extremely convenient to carry around - especially with the smaller 35mm lens I&#39;ve picked out for it.</p>
<p><img src="https://assets.pfy.ch/md/DSCF0013.jpg" alt="" />
<img src="https://assets.pfy.ch/md/DSCF0088.jpg" alt="" /></p>
<p><del>I&#39;ve got some travel coming up soon, so I&#39;m really looking forward to getting some proper use out of the camera. I&#39;ll be posting more pictures soon.</del> <a class=""  href="../urbex/assorted/tokyo-2024.html">I took this camera to Tokyo and got some great photos</a>.</p>
<p><img src="https://assets.pfy.ch/md/DSCF0081.jpg" alt="" />
<img src="https://assets.pfy.ch/md/DSCF0090.jpg" alt="" /></p>
<p>I&#39;m also still getting used to all the settings in the camera. Right now some of my pictures are coming out a bit dark or even over exposed, but it&#39;s all things I can learn with time.</p>
<p><img src="https://assets.pfy.ch/md/DSCF0080.jpg" alt="" />
<img src="https://assets.pfy.ch/md/DSCF0012.jpg" alt="" />
<img src="https://assets.pfy.ch/md/DSCF0007.jpg" alt="" />
<img src="https://assets.pfy.ch/md/DSCF0085.jpg" alt="" /></p>
 ]]></content>
                </entry><entry>
                  <id>https://pfy.ch/programming/pipewire-steam-remote-play.html</id>
                  <title>Pipewire &amp; Steam Remote Play</title>
                  <published>2024-01-15T06:00:49.947Z</published>
                  <updated>2024-06-04T23:16:58.000Z</updated>
                  <link>https://pfy.ch/programming/pipewire-steam-remote-play.html</link>
                  <emoji>💻</emoji>
                  <content type="html" xml:base="https://pfy.ch/programming/pipewire-steam-remote-play.html"><![CDATA[ <p>This will be a quick one, but I&#39;ve been having this long term issue with Steam Remote Play on both my Steam Deck &amp; Apple TV. When I launch a game with remote play, I get no desktop audio and Steam is monitoring my Microphone input as if it was my desktop audio.</p>
<p>I recently installed OBS and ran into the same issue, so I decided to look into it. The culprit was Easy Effects.</p>
<p>If you have Easy Effects installed, Open &quot;Preferences&quot; and then un-check &quot;Process All Input Streams&quot;. After a reboot &amp; changing OBS to &quot;Default&quot; on the desktop audio monitor my sound issues were fixed.</p>
 ]]></content>
                </entry><entry>
                  <id>https://pfy.ch/programming/packaging-for-arch.html</id>
                  <title>Packaging for Arch Linux</title>
                  <published>2023-11-19T01:26:43.472Z</published>
                  <updated>2024-06-04T23:16:58.000Z</updated>
                  <link>https://pfy.ch/programming/packaging-for-arch.html</link>
                  <emoji>💻</emoji>
                  <content type="html" xml:base="https://pfy.ch/programming/packaging-for-arch.html"><![CDATA[ <p>The <a class="externalLink" target="_blank" href="https://aur.archlinux.org/">AUR</a> is insanely convenient for distributing and downloading software on Arch Linux. A while ago I wanted to package and distribute the modified version of <a class="externalLink" target="_blank" href="https://github.com/exch-bms2/beatoraja">beatoraja</a> - <a class="externalLink" target="_blank" href="https://github.com/wcko87/lr2oraja">lr2oraja</a>. </p>
<h2 id="why-did-i-make-a-package">Why did I make a package?</h2>
<p>LR2oraja has modified timing windows to more accurately mimic the timing of the extremely old abandon-ware game Lunatic Rave 2 (LR2). </p>
<p>People still play LR2 &amp; both games play the same &quot;levels&quot; so comparing scores between them when both have different timing windows is impossible. With Beatoraja you can simply download the repo and run the shell script to play but lr2oraja only provides a patched <code>jar</code> file. I wanted an easy way for my friends and I to install this game on our linux systems. Since we commonly ran into issues with java versions and portaudio issues.</p>
<h2 id="creating-a-pkgbuild">Creating a PKGBUILD</h2>
<p>Following the Arch Wiki page with the <a class="externalLink" target="_blank" href="https://wiki.archlinux.org/title/PKGBUILD">same name</a>, I was able to generate a basic PKGBuild file that did what I wanted. Bellow is the current PKGBUILD (minus the functions) as of the writing of this article.</p>
<pre><code class="hljs language-toml"><span class="hljs-comment"># Maintainer: Pfych &lt;contact at pfy dot ch&gt;</span>
<span class="hljs-attr">pkgname</span>=lr2oraja
<span class="hljs-attr">pkgver</span>=build6711481041
<span class="hljs-attr">pkgrel</span>=<span class="hljs-number">2</span>
<span class="hljs-attr">pkgdesc</span>=<span class="hljs-string">&quot;The latest build of beatoraja, but compiled using LR2 judges and gauges.&quot;</span>
<span class="hljs-attr">arch</span>=(<span class="hljs-string">&#x27;x86_64&#x27;</span>)
<span class="hljs-attr">depends</span>=(<span class="hljs-string">&#x27;liberica-jre-8-full-bin&#x27;</span> <span class="hljs-string">&#x27;portaudio&#x27;</span>)
<span class="hljs-attr">makedepend</span>=(<span class="hljs-string">&#x27;unzip&#x27;</span>)
<span class="hljs-attr">source</span>=(
  &quot;https://github.com/wcko87/lr2oraja/releases/download/${pkgver}/LR2oraja.zip&quot;
  &#x27;https://github.com/pfych/lr2oraja-pkgbuild/releases/download/skin/skin.zip&#x27;
  &#x27;https://github.com/TNG-dev/tachi-beatoraja-ir/releases/download/v3.0.0/bokutachiIR-3.0.0.jar&#x27;
  &#x27;libjportaudio.so&#x27;
  &#x27;beatoraja.sh&#x27;
  &#x27;lr2oraja-icon.png&#x27;
)
<span class="hljs-attr">sha256sums</span>=(
  &#x27;5f6dceccaeb0e786dd1658c1b481e8db114bc90a8a1056bf65753e8936ed3369&#x27; <span class="hljs-comment"># LR2oraja.zip</span>
  &#x27;ef23b516537b4f52c306fd61ab9c4197192c06b7202b3b27b63481fec1042a26&#x27; <span class="hljs-comment"># skin.zip</span>
  &#x27;3754959d5d6f121dbeed3a78dec2b91a26e915ff4ce68fdee4262b89ad150cb9&#x27; <span class="hljs-comment"># bokutachiIR</span>
  &#x27;a65d1290d3ee7710f9327c040e6369bf7587eb3609835ed782caaf0ac02d84ed&#x27; <span class="hljs-comment"># libjportaudio.so</span>
  &#x27;a1c1d6ee606d042934ec1ee503a9300d5b225f3c4031be144a3c5bca24f0d042&#x27; <span class="hljs-comment"># beatoraja.sh</span>
  &#x27;0ec1382690cd847055d1b8e6da36ad6846598b45b25acca5eb5e301a5048da03&#x27; <span class="hljs-comment"># lr2oraja-icon.png</span>
)
<span class="hljs-attr">license</span>=(
  &#x27;GPL3&#x27;
  &#x27;GPL3&#x27;
  &#x27;MIT&#x27;
  &#x27;unknown&#x27;
  &#x27;unknown&#x27;
)

prepare() {}

build() {}

package() {}
</code></pre><p>Going from top to bottom:</p>
<ul>
<li><code>pkgname</code> is the name of the package users will type to install the package, and what will appear on the AUR.</li>
<li><code>pkgver</code> is the version of the package that will be displayed on the AUR. This can also be referenced with <code>${pkgver}</code> elsewhere in the file. This is convenient if you pull from git or an external source to get a specific released file.</li>
<li><code>pkgrel</code> is the build-version of the package. You bump this value if you change the contents of the build script or a dependency. If you&#39;re <code>pkgver</code> is <code>1.0.0</code> and your <code>pkgrel</code> is <code>2</code> the final version number will appear on the AUR as <code>1.0.0-2</code></li>
<li><code>arch</code> is an array of architectures the build file supports</li>
<li><code>depends</code> is an array of dependencies the package <em>requires</em> to run</li>
<li><code>makedepend</code> is an array of dependencies that are <em>only required</em> during build</li>
<li><code>source</code> is an array of files that are used by the build, if there are any URLs they will be fetched during the installation process</li>
<li><code>sha256sums</code> is an array of <code>sha256</code> hashes for each of the files in <code>source</code> if any of these do not match during the build the package will fail to install.</li>
<li><code>license</code> is an array of licenses for each of the files in <code>source</code></li>
</ul>
<p>After these definitions you are then able to define functions that run during the packaging process.</p>
<h3 id="prepare">Prepare</h3>
<p>Define actions in this function that set up required files for the build step. beatoraja does not function without a skin folder so in my <code>prepare</code> function I extract the downloaded skin file fetching during setup.</p>
<pre><code class="hljs language-bash"><span class="hljs-function"><span class="hljs-title">prepare</span></span>() {
  <span class="hljs-comment"># Beatoraja will fail to load without a default skin</span>
  unzip -o skin.zip
}
</code></pre><h3 id="build">Build</h3>
<p>Define actions in this function to build your packages files, since we&#39;re downloading a zip containing a binary we don&#39;t need to run any compilers or any other actions here, so we can just extract the file.</p>
<pre><code class="hljs language-bash"><span class="hljs-function"><span class="hljs-title">build</span></span>() {
  unzip -o LR2oraja.zip
}
</code></pre><h3 id="package">Package</h3>
<p>The <code>package</code> function actually moves built files around and &quot;installs&quot; the package. </p>
<p>Here we create required folders in <code>$pkgdir</code>, <code>$pkgdir</code> is a global variable that you can use to know where packages will be installed. You never refer to a path directly. The other useful global is <code>$srcdir</code> which is the path where <code>build()</code> and <code>prepare()</code> run.</p>
<pre><code class="hljs language-bash"><span class="hljs-comment"># Create required directories</span>
<span class="hljs-built_in">cd</span> <span class="hljs-string">&quot;<span class="hljs-variable">$srcdir</span>/&quot;</span>
<span class="hljs-built_in">mkdir</span> -p <span class="hljs-string">&quot;<span class="hljs-variable">$pkgdir</span>/opt/beatoraja/ir&quot;</span>
<span class="hljs-built_in">mkdir</span> -p <span class="hljs-string">&quot;<span class="hljs-variable">$pkgdir</span>/usr/lib&quot;</span>
<span class="hljs-built_in">mkdir</span> -p <span class="hljs-string">&quot;<span class="hljs-variable">$pkgdir</span>/usr/share/applications&quot;</span>
<span class="hljs-built_in">mkdir</span> -p <span class="hljs-string">&quot;<span class="hljs-variable">$pkgdir</span>/usr/share/pixmaps&quot;</span>
</code></pre><p>We then move all the files we created during <code>build()</code> and <code>prepare()</code> into those newly created directories &amp; set permissions.</p>
<pre><code class="hljs language-bash"><span class="hljs-comment"># Move all required Beatoraja Files</span>
<span class="hljs-built_in">cp</span> libjportaudio.so <span class="hljs-string">&quot;<span class="hljs-variable">$pkgdir</span>/usr/lib&quot;</span>
<span class="hljs-built_in">cp</span> beatoraja.jar <span class="hljs-string">&quot;<span class="hljs-variable">$pkgdir</span>/opt/beatoraja/beatoraja.jar&quot;</span> 
<span class="hljs-built_in">cp</span> -r skin <span class="hljs-string">&quot;<span class="hljs-variable">$pkgdir</span>/opt/beatoraja&quot;</span>
<span class="hljs-built_in">cp</span> <span class="hljs-string">&quot;bokutachiIR-3.0.0.jar&quot;</span> <span class="hljs-string">&quot;<span class="hljs-variable">$pkgdir</span>/opt/beatoraja/ir&quot;</span>
<span class="hljs-built_in">chmod</span> -R 777 <span class="hljs-string">&quot;<span class="hljs-variable">$pkgdir</span>/opt/beatoraja&quot;</span>
</code></pre><p>We then create a <code>.desktop</code> entry to make the game easier to launch, it&#39;s a bit tedious but this <code>echo</code> append method is consistent.</p>
<pre><code class="hljs language-bash"><span class="hljs-comment"># Create Desktop entry</span>
<span class="hljs-built_in">cp</span> lr2oraja-icon.png <span class="hljs-string">&quot;<span class="hljs-variable">$pkgdir</span>/usr/share/pixmaps&quot;</span>
desktopEntry=<span class="hljs-string">&quot;<span class="hljs-variable">$pkgdir</span>/usr/share/applications/lr2oraja.desktop&quot;</span>
<span class="hljs-built_in">touch</span> <span class="hljs-string">&quot;<span class="hljs-variable">$desktopEntry</span>&quot;</span>
<span class="hljs-built_in">echo</span> <span class="hljs-string">&quot;[Desktop Entry]&quot;</span> &gt;&gt; <span class="hljs-string">&quot;<span class="hljs-variable">$desktopEntry</span>&quot;</span>
<span class="hljs-built_in">echo</span> <span class="hljs-string">&quot;Type=Application&quot;</span> &gt;&gt; <span class="hljs-string">&quot;<span class="hljs-variable">$desktopEntry</span>&quot;</span>
<span class="hljs-built_in">echo</span> <span class="hljs-string">&quot;Terminal=true&quot;</span> &gt;&gt; <span class="hljs-string">&quot;<span class="hljs-variable">$desktopEntry</span>&quot;</span>
<span class="hljs-built_in">echo</span> <span class="hljs-string">&quot;Exec=/usr/bin/beatoraja&quot;</span> &gt;&gt; <span class="hljs-string">&quot;<span class="hljs-variable">$desktopEntry</span>&quot;</span>
<span class="hljs-built_in">echo</span> <span class="hljs-string">&quot;Version=<span class="hljs-variable">$pkgver</span>&quot;</span> &gt;&gt; <span class="hljs-string">&quot;<span class="hljs-variable">$desktopEntry</span>&quot;</span>
<span class="hljs-built_in">echo</span> <span class="hljs-string">&quot;Name=LR2oraja&quot;</span> &gt;&gt; <span class="hljs-string">&quot;<span class="hljs-variable">$desktopEntry</span>&quot;</span>
<span class="hljs-built_in">echo</span> <span class="hljs-string">&quot;Categories=Games;&quot;</span> &gt;&gt; <span class="hljs-string">&quot;<span class="hljs-variable">$desktopEntry</span>&quot;</span>
<span class="hljs-built_in">echo</span> <span class="hljs-string">&quot;Icon=lr2oraja-icon&quot;</span> &gt;&gt; <span class="hljs-string">&quot;<span class="hljs-variable">$desktopEntry</span>&quot;</span>
</code></pre><p>Finally, we create a symlink to the games install location at the users <code>$XDG_CONFIG_HOME</code>, if this isn&#39;t set we create it at <code>~/.config</code>. The last line here just moves the start script into the users bin folder. We create this symlink since most users expect program configuration files to exist in their systems config folder, Since LR2oraja is not linux native all the config files exist in its installation directory - This symlink just connects <code>~/.config/beatoraja</code> to that install location. </p>
<pre><code class="hljs language-bash"><span class="hljs-keyword">if</span> [ -z <span class="hljs-string">&quot;<span class="hljs-variable">$XDG_CONFIG_HOME</span>&quot;</span> ]; <span class="hljs-keyword">then</span>
XDG_CONFIG_HOME=<span class="hljs-string">&quot;<span class="hljs-variable">$HOME</span>/.config&quot;</span>
<span class="hljs-keyword">fi</span>

<span class="hljs-comment"># Create config symlink</span>
<span class="hljs-built_in">ln</span> -sfn <span class="hljs-string">&quot;<span class="hljs-variable">$pkgdir</span>/opt/beatoraja&quot;</span> <span class="hljs-string">&quot;<span class="hljs-variable">$XDG_CONFIG_HOME</span>/beatoraja&quot;</span>

<span class="hljs-comment"># Install LR2oraja</span>
install -D beatoraja.sh <span class="hljs-string">&quot;<span class="hljs-variable">$pkgdir</span>/usr/bin/beatoraja&quot;</span>
</code></pre><h2 id="distributing">Distributing</h2>
<p>Once you&#39;ve set up and configured your <code>PKGBUILD</code> file you need to generate a <code>.SRCINFO</code> file. This can be done with a single command:</p>
<pre><code class="hljs language-bash">makepkg --printsrcinfo &gt; .SRCINFO
</code></pre><p>You can then test your package installation by running:</p>
<pre><code class="hljs language-bash">makepkg
pacman -U myPackageName.pkg.tar.zst
</code></pre><p>Once you know your package is working you can initialise a git repo &amp; push to the AUR. Here are a few gotchas that caught me as well as some tips:</p>
<ol>
<li>Your entire repo &amp; it&#39;s history must be smaller than around 1-2mb. Your repo should contain as little as possible - preferably only text files!</li>
<li><em>Every</em> commit in the repo must result in a successful build. No &quot;WIP&quot; commits.</li>
<li><strong><em>Always</em></strong> remember to bump your <code>pkgrel</code> when making small changes to your build files</li>
<li>If you are working with a frequently changing code base or repo, ensure you configure your <code>source</code> urls and <code>pkgversion</code> in a way where you can avoid having to make major changes to the build file any time a change is made to the original codebase. The LR2oraja <code>PKGBUILD</code> refers only to the static release files on GitHub - where the package version number is the version number used in the releases URL. This allows me to just change the version number to fetch the latest version.</li>
</ol>
<p><a class="externalLink" target="_blank" href="https://wiki.archlinux.org/title/AUR_submission_guidelines">Here's the official guidelines and steps for submitting a package to the AUR</a>.</p>
<p>If everything worked and your package is now appearing on the AUR you can install it using your favourite AUR helper!</p>
<pre><code class="hljs language-bash">yay -Syu lr2oraja
</code></pre><p>If you want to view the final PKGBUILD for LR2oraja you can do so <a class="externalLink" target="_blank" href="https://aur.archlinux.org/cgit/aur.git/tree/PKGBUILD?h=lr2oraja">here</a>.</p>
 ]]></content>
                </entry><entry>
                  <id>https://pfy.ch/programming/opl-mode-one.html</id>
                  <title>OPL, SMB &amp; Mode 1</title>
                  <published>2023-10-29T11:58:40.246Z</published>
                  <updated>2024-06-04T23:16:58.000Z</updated>
                  <link>https://pfy.ch/programming/opl-mode-one.html</link>
                  <emoji>💻</emoji>
                  <content type="html" xml:base="https://pfy.ch/programming/opl-mode-one.html"><![CDATA[ <p>About 6 months ago I picked up a Japanese region PS2 slim while on holidays for $60. I also grabbed a bunch of games. I knew these games weren&#39;t in english, but it couldn&#39;t be that hard to play a game in a foreign language (right?). I was wrong. I was very wrong.</p>
<p>So how could I run these games in English? - <a class="externalLink" target="_blank" href="https://github.com/ps2homebrew/Open-PS2-Loader">OPL</a>! </p>
<p>OPL supports loading games from a variety of methods, but I was most interested in loading them over Ethernet from an SMB Share. I&#39;ve already got SMB running on my home-lab Mac Mini<sup><a id="footnote-ref-1" href="#footnote-1" data-footnote-ref aria-describedby="footnote-label">1</a></sup>, so I decided to give it a go. I spent a few hours fiddling with the PS2, but I could <em>not</em> get it to authenticate with my Mac. Turns out, the PS2 doesn&#39;t support SMBv2 or SMBv3, only SMBv1 &amp; the version of macOS I&#39;m using outright refuses to allow any SMBv1 connections. I ended up setting a Raspberry PI up to serve games on a public Samba share from an external SSD. This worked, but some games outright refused to run over the network - but they&#39;d run perfectly fine off of a USB.</p>
<p>I ended up not using the PI for ages because most of the games I wanted to play had major issues when loading over ETH. It was super tedious though to do everything over USB and games with music, voice acting or cutscenes stuttered like crazy! This is because the PS2 only has USB 1 ports... So the USB ports are slower than the disk.</p>
<p>I ended up just dealing with it, almost contemplating buying a DVD burner and burning the english versions of these games to a new blank disk. I&#39;m glad I didn&#39;t though, because through someone&#39;s offhand comment I found a solution. </p>
<p><strong>MODE 1!</strong></p>
<p>All the games that stuttered or crashed when loading large files miraculously worked! Turns out by setting Mode 1 on a game you tell OPL to prioritise accurate reads over anything else. Load times might be a bit slower but at least it loads.</p>
<section class="footnotes" data-footnotes>
<h2 id="footnote-label" class="sr-only">Footnotes</h2>
<ol>
<li id="footnote-1">
<p>This would cause headaches later <a href="#footnote-ref-1" data-footnote-backref aria-label="Back to reference 1">↩</a></p>
</li>
</ol>
</section>
 ]]></content>
                </entry><entry>
                  <id>https://pfy.ch/demos.html</id>
                  <title>Demos</title>
                  <published>2023-10-07T00:43:27.809Z</published>
                  <updated>2024-06-04T23:16:58.000Z</updated>
                  <link>https://pfy.ch/demos.html</link>
                  <emoji>📝</emoji>
                  <content type="html" xml:base="https://pfy.ch/demos.html"><![CDATA[ <p>This page links to assorted demos created to show off cool effects made with HTML, CSS &amp; JS. They take advantage of my <a class=""  href="programming/projects/sandbox.html">sandbox</a> to allow for code deep-diving if you want. These demos will likely <strong>not work without Javascript</strong> - and they may load assets. If you are weary of data consumption please keep this in mind!</p>
<p>When viewing a demo page - Press &quot;Run Project&quot; to enable Javascript and start the demo. </p>
<p>The latest demo is embedded here:</p>
<iframe height="600px" width="100%" src="/sandbox.html?data=eJzdVm1v2zYQ/isHtYWlzbLlxNk6uQ6wrC1aoAWGYB/i1gMqi5TNjiY9kXKsGv7vPVKSbfklkPtpWIBIyt0999wbj1k7Mz3nTui8ImwJjAzHzjSl+T0VhKafpKBj53YsxnqnVv9mUVqJS0XMI6VQF2eTneaE7m0UU6g+3qZSaLROzPtVF20bAu+i+B/ETfB1CeyeTWeGLzXvS4AfaGJwHF+XwP6SC0RpubgoN6m1nJvs7Mc+dPddfRVvp+3ESmELn9U7B2tjS5ha8CgPIeF0NTCSr5nSLMn9GMtOhQ4hxidNrS7ibCp8pulc1eVqQWPtp5FmMoRet2elpgPTVGaChGC4rfCRET1DmyB4Yf+eUVPvrWCDMvGsGKIiwoVUDN2KEFLKkWBJLU6nkVCJTOe+0jmnISxSqmi6pP412bInWDJ/yRSbMM40ZjljhFBRd4COpY40vSZu0IZeG/DZvyF06u0UhXSrqALtmL4UYZZ5XQXBYlVLbCf50UysQYncGkNP1aKwY3JQsGiiJM80HTQMcCJTHA6ULFaASEZgwrGKVseZoP4xJMEp8RX7hmH3a7LH0nQieZFFLLlE348zVgak6Ur7dqJqs5Rh7r6iHAcqBIGDepSm3QxFrvsjNlM8sh3ESWrDjXkEnV+9080euYHtsFVgM+gnt2eS8o7Y7F44w/ZLc7rfmvKZ9XWGrnfVnK/3simh2V/nCF82J/QbZ4ib7wzfVb8p38MFBbWr8gzjddCc8ckMcc1+NVu224V3kSB4XKYwlzjM+FzSOc433MJrinuQgkwSRbUJklMNTH00dq/lo4AhJBFXZuSNBhfBkqHuz/JEo3oNq9DsIdxlAWwO7O5NoHt2/Rtr2L+xlmOBK11pXD7bK2AIRMaZCa4zpfoNt3He5e+J26pfFi2vQper+QlkYdGyZRQsAXdH6BVd2I/jzjQjShlV6HNnabxaFdbxD87Q+T0uBLdwCvuGESFvlqj/wBTeVzR1W7bspuqtNrjU6DwY3hpqwJ+KvNjAD8hqTTqxZXkA/yiwjllrx9DRAXR0CpqXAQOYSuy12qviqdyuPmLEBH267mHfOyvruozYgy5cefAT+L1B3UX+hIt8z8Wo7uIgjt8Vjik6OZwqjOLnMspD3rMQw5pXkAqER+QB8PoRMKpExcx07AXY2d1xQ/hSWcCJW/r5ugh2Y47lCcPtrf18nR8bftkL6dwJKvzbU1QmuRkcYg5P57ZRFrablgq5Ma9N80kmOC3/qUn+0cTri06nGR1cWIps8T8sRLXxy0qYmwT/J2caL5PAfJthdsLPIuO87fQc8/v35jt5WamJ"></iframe>


<h2 id="all-demos">All Demos</h2>
<ul>
<li><a class=""  href="/sandbox.html?data=eJzdVm1v2zYQ/isHtYWlzbLlxNk6uQ6wrC1aoAWGYB/i1gMqi5TNjiY9kXKsGv7vPVKSbfklkPtpWIBIyt0999wbj1k7Mz3nTui8ImwJjAzHzjSl+T0VhKafpKBj53YsxnqnVv9mUVqJS0XMI6VQF2eTneaE7m0UU6g+3qZSaLROzPtVF20bAu+i+B/ETfB1CeyeTWeGLzXvS4AfaGJwHF+XwP6SC0RpubgoN6m1nJvs7Mc+dPddfRVvp+3ESmELn9U7B2tjS5ha8CgPIeF0NTCSr5nSLMn9GMtOhQ4hxidNrS7ibCp8pulc1eVqQWPtp5FmMoRet2elpgPTVGaChGC4rfCRET1DmyB4Yf+eUVPvrWCDMvGsGKIiwoVUDN2KEFLKkWBJLU6nkVCJTOe+0jmnISxSqmi6pP412bInWDJ/yRSbMM40ZjljhFBRd4COpY40vSZu0IZeG/DZvyF06u0UhXSrqALtmL4UYZZ5XQXBYlVLbCf50UysQYncGkNP1aKwY3JQsGiiJM80HTQMcCJTHA6ULFaASEZgwrGKVseZoP4xJMEp8RX7hmH3a7LH0nQieZFFLLlE348zVgak6Ur7dqJqs5Rh7r6iHAcqBIGDepSm3QxFrvsjNlM8sh3ESWrDjXkEnV+9080euYHtsFVgM+gnt2eS8o7Y7F44w/ZLc7rfmvKZ9XWGrnfVnK/3simh2V/nCF82J/QbZ4ib7wzfVb8p38MFBbWr8gzjddCc8ckMcc1+NVu224V3kSB4XKYwlzjM+FzSOc433MJrinuQgkwSRbUJklMNTH00dq/lo4AhJBFXZuSNBhfBkqHuz/JEo3oNq9DsIdxlAWwO7O5NoHt2/Rtr2L+xlmOBK11pXD7bK2AIRMaZCa4zpfoNt3He5e+J26pfFi2vQper+QlkYdGyZRQsAXdH6BVd2I/jzjQjShlV6HNnabxaFdbxD87Q+T0uBLdwCvuGESFvlqj/wBTeVzR1W7bspuqtNrjU6DwY3hpqwJ+KvNjAD8hqTTqxZXkA/yiwjllrx9DRAXR0CpqXAQOYSuy12qviqdyuPmLEBH267mHfOyvruozYgy5cefAT+L1B3UX+hIt8z8Wo7uIgjt8Vjik6OZwqjOLnMspD3rMQw5pXkAqER+QB8PoRMKpExcx07AXY2d1xQ/hSWcCJW/r5ugh2Y47lCcPtrf18nR8bftkL6dwJKvzbU1QmuRkcYg5P57ZRFrablgq5Ma9N80kmOC3/qUn+0cTri06nGR1cWIps8T8sRLXxy0qYmwT/J2caL5PAfJthdsLPIuO87fQc8/v35jt5WamJ">Intractable 3D HTML cube</a></li>
<li><a class=""  href="/sandbox.html?data=eJyFVdty2zgM/RXUbcZSa9lWLn1QLHfSy+zkoX3Jzr6sdya0RFlsKFJDQnbc1P++IHWJk7S7E9tDAiBwABwgD6MSKzlKRot6+WcpLNCHwbpRWQm6gE83N5CLrQWLLLvjOWgVoa6dijNngiU3wFQOaJiyhTYVGaGGSlQiu4Ozz4tZvVypBTkBkaer0Vbw3Wq0XMxIshxNRpm1FP214Srn5spwBg8rtcJc2FqyfQKF5PeXTsKk2KhIIK9sAhlXyI2XO4MoF4ZnKLQilZZNpbyqYmYjPOAEzk5r5+dAcvXagWjj1NqK9p3hkqHYcv9yJ3IsE7iYn7SxbU3uI0MGOoF4FpMUHlOmxxoZ8rM8mE+APvEEovOLnG/CR03sxaTsFDZjkgfvL07CHtZKTTdG5M+BsbWllLAF5nOZ+6PkBfbnDm88/w/AvyqqL97OMHLqfi89DJy6Krcwfluh9i3Fp7/T6cVJJ1zr+8iKH0JtElhrQz2NSNQpf42KHhG3NkY3Ko+oe9oksGUmiNpL6I0OvkbEl++OLrMZXBew1w3kullLDuiou2Wy4VCxOw62MdzrmbQaSiYLMuHg2eOzhzWzwr5aqUwri+DqfiN+cEjh3Fe0k3uipBQlayqi3HTD8Yvk7vhxf50HY6cfh4/2GREY+R+uiykEFINfGcP2E8h5jWUI6bIta2ut+K4zHQK0DroYwZiGxLvHznSaSWbtN1Y5pGOHenystRyvEI1YE12CscW95OMJ3B4R1R9ly8g3D2dvPa5DfU+EfXo9voW3YUuMIaEpeftCCyBg7nbt6OJTw7ahbXqeRf+TW8cAZ/o0NycZd3xcITX8kyZFU8NOYOlWFEfaALBje7duSlpBRANHmMZATauHGHjk+jeFiXq+lVYG8ek5DaafIcqeON3lTwMKumaZQJqcNw9HGacpkf8DfYnMh8vbcIDb94PVNe21wEHwXTx0dTQcG6P6/vfz/5xCvtKOR1QXfMGjQTitWB2IoQUDu6jTua7+8kORwleG5bQVBQQUWyBUV7/1bZPd0WBoAwWTUhcFRDTuBN9o6vIHZyoK6KL4tH/+fBJgCTHMIPDw6BDPw7BfIF2y836OjwoQU+a+JJIjDOQitB3JDIH1x2AY0LfDrIYTCHwlYld4hz1wbloIKcSX3XGRwum8u7x718E6Dvas4i/H1j1wk9738/HBS1syPtCaokUrkDbV3J3df4BR8re/tN9/Dv8CmBSXug%3D%3D">Fake 3D grass mesh in CSS</a></li>
<li><a class=""  href="/sandbox.html?data=eJyVV1Fv2zgM%2Fis89%2B6SYI3jBu2u8No%2BrCvQDbthWPoyLAMmW3KsTbYMS26SK%2Frfj5Jsx4mT9K5FEocUxY%2FkR0p58lKdCS%2F0rih%2FBE6v596yJEXByrl3M88BNvIilTmrpT35OCePqLuVWSHYCmZxyVh%2BNcFFB9bHMtcs1%2B1%2BuCI9u%2FlsdHDrdFcTlLTaon0EeA%2B4P9GMAsmBCZbhalA844KUoCXolCtIZAkEYsGNkufwhZFYwyei%2BSPz4QGXbDbE5bgTN%2BDNXrhG5lApni%2FgZ6U03M5mkw8zvwUzadA0Ie4GSUuybHO4TzVOSU6FyWc3S3vW9RPlUvXOqrdytJMlsEGC2wdSoqDeC5Ypj1MTtIpLKQSJBIOo0hBjEohQEiL8XspqkWqoiu6OmNyEC4EZZsYYi9xxPul6v%2BLZAlQZYySp1oUKJxOiFNPKL5K1H6eTjE4C83fx%2BiwIplP%2FZ7GYezC5uYpKfP8vMSmYe7dCKkbRMGXcwLVKrDDyIte8ZGIN0drCtbxrUqC6WzpTH%2B4o1447CosTyRVTcP%2Fw90cT9SNnS6v77WDAe3AuiSMW%2BiyZ0oZPcUqEYPmCmV1LFjMEa%2BhJgDLFFzkkpcyAdLdyHD7geKvJ2ufmqfn0Tr1YKezzk7q94QlJqylXhSDrEBLs2jdGQgRiGHPNMhVCzAxwKzdtwJN1Q8ctnctfCFjHP6wAKYK6ECJRlcNpUKxGVlwp5LPCfo1xcY7VMFKUn7jSWERzXUjFTf%2BFmB1hu%2FWNU%2FTBoid8HFOsc%2BxMYimqLK%2BVGVmNl5zqNISLGtkG7F8XjUQ%2BsjIRchlCyilljXUkS2qCOC9WoKTgFE6QrVvKcUkorzBRl8WqVazGKiXUbBfg%2FwVam5exndZriCoQLxojZszaZFrnocmEGaZNNjAfhFIkjnUCZ69bTy2ISGotM9xoH9DnY1trttJjW%2FGtevatmsHRWmakXPC8dX0edGC1gLfAbrJMKi07fsyb9XdSTyrrZMMCEmFQlXYsaBy68PqMOMIHWxn%2Bj0XWZs5Z1SRp%2BdsjdIaxNsI21ojEvxY4JXMawkmSJLUXx5o9xdjhjMmOewsg2CA8xh3TK1vHR1OQP8OQJLrJ3VazRELGv9oq9NphE%2Bxlp1iuvDXGTr12gkZcZJeNTXjnne3iqlQSk1JI3iEZypthMhjsEKIb6g75esXpMC7YZLXX3G0Yhm84D3%2BacYh74%2FFOcp7ZM3%2FGtBnSCq7hCWhlOxTTMA2CU3vuhXji4L1iSUqq5t4pMKKs37mHT2yMLJEVHtXogcq4MlcJf8H0nbtVvF2%2Fp8NBPX8HI995ZcNvT828HFQ5npCD5%2B%2BnfUgjkxQH143Lazjowy4YmKHbMfiEnf%2BSjRkPg9GWWX0Ze9m0LtPGvG7nI4ZuRQeoE9w7br9oWDeBtd%2BH%2Ba2hqallV2j2sgpM6q09WL%2FgsBj2k7VtjYIjlo0tnjj37h5yDT9iIuKhYSmM4fenPjC%2FvrK82nFZy5%2Fx2PyBRLKh%2BkqvBfORHZ9LifTR6%2BHALRucbrxajghmbkEuh%2B%2FkMkcoCV7qmNOkrfwrKgInxGGhSR6zBzlb8sJk%2FiIwW3XL4WOP3eG9Sn%2FExSxn5XCQSTzRcVrliGHIjG4E1zeuT3cQ6LJidrJuubc2vrvefMWutPjboh9wWBX73CUw7LocNdNifyZQ47Ro1oU07qXipga5HnVmqyvJpn%2BbkbSv5LtlxXJf2tIe7HIzBvFXjcJr4Q68Vz14V%2F8HXsuTo66bETnCGYlnCNc4JgPzjD%2BLmBd%2Bs1%2FO8BV435%2F%2FBWRl7Lc%3D">Drawer over content</a></li>
<li><a class=""  href="/sandbox.html?data=eJyVVWtv2jAU%2FStWtqkw5WFa%2BkpHP3Trh0mbNrXSVLRMq0mc4C6JkW1eQvnvu7ZDSBidWhDEnHvvucfHDzbOVBW5EzofErZAcU6kHEVOwVaKzOgPRpeRcx2VkeqElSDxnzoAIVZkdhQpkiuIE6GWXEBGVCLzYgmgMV9QscOkiAGcKjWTYRCkQ38ST5iSfsyLABgDMhicnp%2BfXQxPT34PzvynWbatDayiACTByD4d14mlhHn4Le1ooxMTJmc5WYcozenqSiMkZ1npMUULGaKYlooKgz%2FNpWLp2os5QKXqxKaUZVOABhi%2F0wBAvvHBNjnUJlIZmUHF2Wz7%2B5nGJvbG%2BLNli1RBVt6SJWoaohOMG4pI8ckTjZWXMi0QlBJWNrEJX3lyShK%2BDBGG9wAK4fEGY3zZJIHsUqZcFB4XLGNlZ56RqvQDvirw9ElbCk2k0otfzNAI9RYkdxGogy9W9tHoGn0lauoD0rMDVjY5fZsEzDWJmeMIJTyeF9DUz6i6zake3qw%2FJ71ml%2FSNL7YIeO64IopC4RBfteBvaSqpAvgYt%2FInfF4mrMwAN2y6yU2NfcwZ9LoDA3t9W6ETSJLcLgD%2FwiQsPBWgo%2BBzSQuIRo6LelRHzVzN%2Btg%2BguZEsQV90I20OzbNj02PB%2BQ1SnwwqxmbRXURNlPcoxofpBq3qdYtKrspt1w7NuPWw3cq9MIC5U5psKfjar9q%2FG%2FVuF1lW3barT7RTFAqocRsgDTnXPR2q%2FZ%2BX5DXWtIAHbeNWL%2BQa%2FwCrtW9OQnNJukS1vBhcXVwX9xrCMf%2FIzSUZueVrIDkXn3sm6MZokdLc5L0sIsGsMYuervZWl0lNOujJsOGTYa3bqc8upYX7oX7%2Blp4hJyOMxVcEbpuH2vdHaeWp3I3yPKlLM9DdARKl0Qk8sit8WQuYMtwuFKGGG9BSiTsHEjPWUmJODJwpX2oXnAG%2BVyZI9g%2Bfa92Dj%2FjF37OJDP3rgmXrzfh4oAJMKAeTKpjA1y18O%2FBFNy2WI%2B1Tif86Qzgh%2F5g51f1FzDBqR8%3D">3D Transform on image with shadow</a></li>
<li><a class=""  href="/sandbox.html?data=eJxtUlFv2jAQ%2Fiu3dFOCBGn20GlKoQ9UrbRp2sO0h1aAVBNfiDfHjmwHyhD%2FfWebwkonEWF%2F5%2B%2Fuu%2B9ulzSulUmZjLlYQyWZtZN5sha4mSc3czV34%2B7mZyMs0A%2FbJXKOHIQCpkDcG9biu%2FFld3qIwLUD2%2Bhecqi1lHoDW90baHVvEbSq0N8BFVtKhK9szWxlROdOabwOwUlEoJCK8SVBFIv%2FyTCprCXBuRcJO8%2FptBVOaFWCQcmcWOO1h5f6eWTFH6FWJSy14WhGBMUQq36vjO4VL%2BGirusANihWjSvhY1F8CEDHOA%2Fsz12k6TWamnoqoRHkhJqrPcHqInZ3poUtrZa9i1qc7kq4OuSVWLvTbSO4a6jqp0ORo4wX4LXYojg0FxoyjIveHjXvyZ5f3p1KK%2Bui6%2FdhDGhgQsOp%2BhaVy1fo7iT643T7hWdHsweUI1KDuxOY5Xme%2FYdmp9tbvyzfaQWy1D9OB4PFrFh4ccrfczLvbk1PvwnrUKHJ0lCkJRPTIWToYwOY3ETfTlWnvlNmBFqqHzJR2QDSLG6lINoPrFw2CD6cuC%2BzfyRaSJ5X4fEjjM7y5tvrt7SHM9rDW9qz723uXrmaMyVa5jCbhTYOk356vzvK2XfPT0OIwTj5f6IPPupj%2B8UQDhl4b1hcoauiGEasFlKWkNbabJjhNj3AyGxY0JQOOBIqDakGtAj00S7YTgpH61D4syOZSTkLl%2Fgt9n8B2mlsgg%3D%3D">Simple dot following cursor</a></li>
</ul>
 ]]></content>
                </entry><entry>
                  <id>https://pfy.ch/programming/projects/sandbox.html</id>
                  <title>Sandbox</title>
                  <published>2023-08-25T03:50:25.000Z</published>
                  <updated>2024-06-04T23:16:58.000Z</updated>
                  <link>https://pfy.ch/programming/projects/sandbox.html</link>
                  <emoji>💻</emoji>
                  <content type="html" xml:base="https://pfy.ch/programming/projects/sandbox.html"><![CDATA[ <p>For fun, I thought I&#39;d look into making my own online editor for <code>HTML</code>, <code>JS</code> and <code>SCSS</code>. I ended up with a page I&#39;m relatively happy with and I learnt a bit along the way. The page features SCSS compilation as well as saving to the URL.</p>
<p><a class=""  href="/sandbox.html">You can check the page out here</a></p>
<p>By default – Javascript is disabled and not evaluated until you enable it. This can be seen in <a class=""  href="/sandbox.html?data=eJxNUl1v2zAM%2FCusig0t0LjZ2%2BCmAbpgwB72BWQPLeo%2ByBYdq1VEQaSTeUX%2B%2ByR7WfMmHU93R1KvqpOtU6VaGLuDxmnm20rtLO4rtax8JYuw%2FBmRGSp15xysyGClQAgyB557FmgStrgOR%2FoD9dBoD9KhTzXndGDMN%2Fjy69tXeA%2Fr1XoNeyvdCAoFiHbTCdxD3YuQ5xOxHyGJDNRHMLhDRwFj0vRMDs9g0MPZxF1cp%2FhLdaUa5tRMMYZ7zRo1%2FZ6x%2FWP9poSaosE4S9DNWNLNyyZS700J523bjmCHOUsJH%2BbzdyMQtDHj649hfJahSbqSlrzMWr21biiBtecZY7ST0GHiFrVOY%2FvHD8RWLPkSdJ1a6AVvpsLeGumy6eQhpzn%2BQ6d5I5ojOjUVtbE9n%2BRO%2Foc0kOc8j8eiKC4MNf0WvRQblM8O85E%2FDau88u96ixfHtV9ePj3OnwqWwWHx5rkiRxFu0z8I1r9USh3%2BAk%2BJyeg%3D">this demo</a> which will only set the background to pink once you enable JS and interact with the editor. Be warned though, once Javascript has been enabled – if a script loops endlessly you may lock up your browser. Don&#39;t run anything here you wouldn&#39;t run yourself in the console!</p>
<p>Saving to URL was an unexpected issue for this project, Most URLs can only be about 2000 characters long<sup><a id="footnote-ref-1" href="#footnote-1" data-footnote-ref aria-describedby="footnote-label">1</a></sup>! This was overcome by using a GZIP implementation to first compress the data before inserting it into the URL parameter. This obviously doesn&#39;t solve every use case, however the editor will now warn you when trying to load a code snippet that is too long from the URL.</p>
<p>I used the following packages to help with development:</p>
<ul>
<li><a class="externalLink" target="_blank" href="https://www.npmjs.com/package/sass">sass</a> - Compile SCSS into CSS</li>
<li><a class="externalLink" target="_blank" href="https://github.com/nodeca/pako">pako</a> - GZip Implementation</li>
<li><a class="externalLink" target="_blank" href="https://prismjs.com/">prism</a> - Web native syntax highlighting</li>
</ul>
<p>Some of the main hurdles I encountered while working on this project were:</p>
<ul>
<li>There are no children inside a <code>&lt;textarea /&gt;</code> element, to modify styles of written content you must first mirror the content to another element. Syncing the display &amp; scroll state of these two elements to create a seamless editing experience was a bit of a challenge. However once they were synced up, prism did a majority of the heavy lifting.</li>
<li>Compressing the code into a URL safe string was quite difficult. Initially I implemented an LZ77 compressor which worked quite well with plain strings however it really only lowered string size by 5-10%. I was told to look into GZip, this implementation seemed much too complex to do myself, so I opted into using an existing solution. The most popular library I could find called pako only worked with <code>UInt8Buffer</code>&#39;s which I had never worked with before. It was interesting to work with the <code>string</code> <code>-&gt;</code> <code>UInt8Buffer</code> and back conversion process.</li>
</ul>
<p>An unexpect side effect from this project is I now have inline code blocks &amp; previews using plain iFrames! I don&#39;t know if this is a good or bad thing yet haha.</p>
<iframe height="600px" width="100%" src="/sandbox.html?data=eJxtUlFv2jAQ%2Fiu3dFOCBGn20GlKoQ9UrbRp2sO0h1aAVBNfiDfHjmwHyhD%2FfWebwkonEWF%2F5%2B%2Fuu%2B9ulzSulUmZjLlYQyWZtZN5sha4mSc3czV34%2B7mZyMs0A%2FbJXKOHIQCpkDcG9biu%2FFld3qIwLUD2%2Bhecqi1lHoDW90baHVvEbSq0N8BFVtKhK9szWxlROdOabwOwUlEoJCK8SVBFIv%2FyTCprCXBuRcJO8%2FptBVOaFWCQcmcWOO1h5f6eWTFH6FWJSy14WhGBMUQq36vjO4VL%2BGirusANihWjSvhY1F8CEDHOA%2Fsz12k6TWamnoqoRHkhJqrPcHqInZ3poUtrZa9i1qc7kq4OuSVWLvTbSO4a6jqp0ORo4wX4LXYojg0FxoyjIveHjXvyZ5f3p1KK%2Bui6%2FdhDGhgQsOp%2BhaVy1fo7iT643T7hWdHsweUI1KDuxOY5Xme%2FYdmp9tbvyzfaQWy1D9OB4PFrFh4ccrfczLvbk1PvwnrUKHJ0lCkJRPTIWToYwOY3ETfTlWnvlNmBFqqHzJR2QDSLG6lINoPrFw2CD6cuC%2BzfyRaSJ5X4fEjjM7y5tvrt7SHM9rDW9qz723uXrmaMyVa5jCbhTYOk356vzvK2XfPT0OIwTj5f6IPPupj%2B8UQDhl4b1hcoauiGEasFlKWkNbabJjhNj3AyGxY0JQOOBIqDakGtAj00S7YTgpH61D4syOZSTkLl%2Fgt9n8B2mlsgg%3D%3D"></iframe>

<section class="footnotes" data-footnotes>
<h2 id="footnote-label" class="sr-only">Footnotes</h2>
<ol>
<li id="footnote-1">
<p><a class="externalLink" target="_blank" href="https://stackoverflow.com/a/417184/3673022">https://stackoverflow.com/a/417184/3673022</a> <a href="#footnote-ref-1" data-footnote-backref aria-label="Back to reference 1">↩</a></p>
</li>
</ol>
</section>
 ]]></content>
                </entry><entry>
                  <id>https://pfy.ch/thoughts/cozy.html</id>
                  <title>Cozy spaces</title>
                  <published>2023-08-09T03:50:25.000Z</published>
                  <updated>2024-06-04T23:16:57.000Z</updated>
                  <link>https://pfy.ch/thoughts/cozy.html</link>
                  <emoji>🤔</emoji>
                  <content type="html" xml:base="https://pfy.ch/thoughts/cozy.html"><![CDATA[ <p>I recently moved into a new place and I find myself being more content and cozy. If you asked my friends or co-workers about my last unit you&#39;d probably here the phrase &quot;Crack Home&quot; thrown around a bit. It was a crack house, but filled with care and love, like a home - a crack home.</p>
<p><img src="https://assets.pfy.ch/md/CRO2648-R1-01-36A.jpg" alt="" /></p>
<p>The apartment was an old red-brick, renovated housing commission. By renovated I mean; the owner threw a new lick of paint on the walls and replaced half the tiles in the bathroom before going: &quot;Ok, lets get a Tennant in!&quot;. </p>
<ul>
<li>Some of the lights didn&#39;t work, and were bare LEDs soldered in a fixture (so I couldn&#39;t replace them). </li>
<li>The bathroom ceiling was dropped to add an exhaust fan (that exhausted back into the bathroom through a hole in the ceiling) </li>
<li>There was a solid mould problem</li>
<li>My neighbour above me <em>LOVED</em> to fight his furniture at 1am</li>
</ul>
<p>This was my first unit living alone, and it was my own place. I took care of it. I decorated it with books, hung skateboard decks off the walls, had a dope sound system and TV hooked up to my PC. I really tried to make it my own. I lived there for almost 3 years with rent only being raised once - $290 a week to $320 a week (with no lease). It was a pretty good setup.</p>
<p>I made so many memories in that unit. Both good and bad. But I&#39;m not upset I&#39;m not there anymore. I mean sure, at the time locking the door for the last time ever was a little sad. But I don&#39;t think I&#39;d ever go back.</p>
<p>It&#39;s the small things you don&#39;t think about.</p>
<ul>
<li>I can open my blinds without worrying about someone breaking into my unit</li>
<li>I can wake up to the sun</li>
<li>I have a kitchen I enjoy cooking in</li>
<li>I have space to store my stuff</li>
</ul>
<p>These are all things my last unit didn&#39;t have that I didn&#39;t think I needed. I had a dope place on my own for cheap, so what if I cant open the blinds or get sunlight? My kitchen has space for a microwave?! Why would I need a good cook-top?</p>
<p>Eventually though, I did end up moving. I started to feel more comfortable moving into a place <em>with</em> someone else and splitting the bills. Maybe I don&#39;t need my own cave? It&#39;s possible to have my own space while also sharing part of it with someone else?</p>
<p>Today I caught myself cleaning. Like just instinctively, not like picking shit up but I mean like I was scrubbing tiles. I never would have done this at the old place, because I think deep down I really didn&#39;t actually like that place - So I didn&#39;t take care of it. This new unit has sun, space, places to put my shit, a bathroom that doesn&#39;t spawn mould like it&#39;s life depends on it. It&#39;s genuinely improved my mental moving, even if it is an extra $5 more rent...</p>
<p>I used to joke with people that I take better care of other peoples place than I do of my own, and I don&#39;t think that applies any more. I actually care about this space, it makes me feel comfortable. I don&#39;t unconsciously flee from it on weekends or during time off, and its been really nice to recognise all this now that I&#39;ve moved. </p>
 ]]></content>
                </entry><entry>
                  <id>https://pfy.ch/programming/serverless-please.html</id>
                  <title>I want to recommend serverless</title>
                  <published>2023-07-10T03:50:25.000Z</published>
                  <updated>2024-09-26T06:09:25.000Z</updated>
                  <link>https://pfy.ch/programming/serverless-please.html</link>
                  <emoji>💻</emoji>
                  <content type="html" xml:base="https://pfy.ch/programming/serverless-please.html"><![CDATA[ <div class="banner">
**Update 2024/09/26:** I end up recommending SAM at the end of this post. Honestly I ended up just sticking with Serverless Framework v3. It just sorta works, especially locally. I don't see myself ever using SAM or CDK.
</div>

<p>To preface this rant, I feel the need to clarify that I absolutely love working with Serverless and AWS professionally. Over the course of more than 2 years, I have deployed numerous applications across various industries, gaining valuable experience. Serverless has allowed us and our clients to scale and grow into multi-million dollar ARR businesses without ever having to worry about infrastructure.</p>
<p>This said however, I still don&#39;t like to recommend this stack to people. I always say something along the lines of:</p>
<blockquote>
<p>&quot;Oh Serverless is great if you&#39;re being <em>paid to learn it</em>.&quot;</p>
</blockquote>
<p>Hopefully, in this post I can describe some of my pain points with Amazons official offerings around Serverless, &amp; Why I&#39;ll be using SAM over the third party Serverless Framework for most of my personal projects from here on out.</p>
<div class="banner">
The following post is all my own opinion & does not reflect on the opinions of any past, present or future employers. I've worked with Serverless stacks both Professionally, and independently. These opinions are wholly my own.
</div>

<h2 id="serverless-framework">Serverless Framework?</h2>
<p>If I&#39;m being completely honest, deploying <em>to</em> a serverless environment is a complete shit-show. There&#39;s heaps of ways to do it, they&#39;re all slightly different, but they all compile down to one thing in the end: <a class="externalLink" target="_blank" href="https://aws.amazon.com/cloudformation/">Cloudformation</a>.</p>
<p>In almost all my projects whether it be at work, or personally, I used the <a class="externalLink" target="_blank" href="https://serverless.com/">Serverless Framework</a>. It abstracts writing cloudformation in relation to Lambdas&#39; and makes it easier to focus on writing code. However, Serverless Framework is not AWS Native, and I only ever deploy to AWS. Also, a lot of the functionality I take advantage of in Serverless Framework is based on community plugins which can break at any time with zero warning. I want my infrastructure, which is critical to my work, to be backed by a large body &amp; stable, not something maintained by someone in their free time.</p>
<p>Serverless also seems like they&#39;re trying to push their Dashboard and extra tooling, which I don&#39;t need.</p>
<h3 id="serverless-framework-ls">Serverless Framework L's</h3>
<ul>
<li>Lots of functionality I use is based entirely on community plugins which can break</li>
<li>Serverless Framework tries to support all cloud providers when I only need one</li>
<li>Serverless Framework has their own motives and goals with the library to sell their dashboard</li>
</ul>
<h3 id="serverless-framework-ws">Serverless Framework W's</h3>
<ul>
<li>Easy to use straight up plain Cloudformation if required</li>
<li>Lots of community support &amp; plugins</li>
<li>Can easily start focusing on your applications code</li>
</ul>
<h2 id="cdk">CDK?</h2>
<p><a class="externalLink" target="_blank" href="https://docs.aws.amazon.com/cdk/v2/guide/home.html">CDK</a> instantly gets written off by me just by glancing at its docs. It&#39;s an extreme abstraction of Cloudformation. Where you write Typescript, run a magic CLI command, and it spits out Cloudformation files. Any sort of black box transformer seems like an issue waiting to happen, especially when it&#39;s tied to a completely different language. </p>
<p>I don&#39;t want another layer that can potentially break when deploying my infrastructure. That&#39;s what I&#39;m trying to avoid &amp; one of the reasons I want to swap from the Serverless Framework. No matter the abstraction you <strong><em>will</em></strong> have to manually write and manage your cloudformation at some point. Adding extreme abstractions on top of it only delays the inevitable.</p>
<p>CDK also requires the use of AWS SAM to run locally, so I don&#39;t really understand at a glance why I&#39;d want to use CDK over SAM? Personally, I haven&#39;t used CDK at all. I jumped straight into SAM, but from the brief foray into CDKs docs I don&#39;t feel like I&#39;d ever use it.</p>
<h2 id="sam">SAM</h2>
<p><a class="externalLink" target="_blank" href="https://aws.amazon.com/serverless/sam/">AWS SAM</a> is <em>so</em> close to being what I want it to be. It&#39;s an extremely light abstraction of Cloudformation at a glance, and it allows for extremely easy local testing &amp; deployment. However, it is not without its downsides.</p>
<p>SAM Requires the use of Docker, which the Serverless Framework does not. This adds an extra hurdle to initial development when getting started. I&#39;m not speaking for everybody here, but personally I find docker (on macOS<sup><a id="footnote-ref-1" href="#footnote-1" data-footnote-ref aria-describedby="footnote-label">1</a></sup>) a bit of a nightmare to set up. Do I install docker desktop? or docker engine? Do I install it through <code>brew</code> or do I use the pkg on the website? These are all things that as a developer who&#39;s done it before are trivial questions, but I can see these being asked by coworkers who have never used docker before. This isn&#39;t a slam on SAM, but it is a pain point I do foresee briefly in the future if professionally the company I worked for migrated from Serverless Framework to SAM.</p>
<p>Another major pain point with SAM for me was getting DynamoDB to work. In a perfect world DynamoDB <em>should</em> just work right out of the box and get stood up if your template file contains table definitions. Serverless Framework had<sup><a id="footnote-ref-2" href="#footnote-2" data-footnote-ref aria-describedby="footnote-label">2</a></sup> a plugin which did this. Instead, <a class="externalLink" target="_blank" href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/DynamoDBLocal.DownloadingAndRunning.html">Amazon recommend</a> you run the DynamoDB <code>.jar</code> locally, or stand up a docker container yourself. The docker steps also add an extra layer of confusion for new developers since it recommends adding your apps image (what?) to the <code>docker-compose</code> and linking them. If you&#39;re just running a SAM app locally all you need to do is create a virtual docker network and put Dynamo&#39;s container on it, then start sam with an extra flag:</p>
<pre><code class="hljs language-bash">sam <span class="hljs-built_in">local</span> start-api --docker-network &lt;network-name&gt;
</code></pre><p>This is not mentioned in the docs, and no error is thrown on SAMs end about this. It&#39;s up to the developer to notice there&#39;s no connection, realise it&#39;s due to SAM actually being in a docker container, and then knowing how to put both containers on the same network...</p>
<p><a class="externalLink" target="_blank" href="https://stackoverflow.com/a/49025379">This stackoverflow answer</a> is how I found out about this.</p>
<p>Oh, by the way there&#39;s no way to seed table definitions from SAM into this container now that it&#39;s stood up. Here&#39;s the gross script I whipped up to seed from a template file into the container. There is probably a better way, but I&#39;d rather focus on writing my actual applications code. (SAM should do this if there are table definitions!!!!)</p>
<pre><code class="hljs language-bash"><span class="hljs-comment"># Convert the Template YAML into JSON that create-table expects</span>
<span class="hljs-comment"># Dump this to a temp file so its easy to &quot;while read&quot; later</span>
yq -o=json <span class="hljs-string">&#x27;.Resources | filter(.Type == &quot;AWS::DynamoDB::Table&quot;)&#x27;</span> ../template.yaml \
  | jq \
    <span class="hljs-string">&#x27;.[] | {
      TableName: &quot;local\(.Properties.TableName[1][1])&quot;,
      KeySchema: .Properties.KeySchema,
      AttributeDefinitions: .Properties.AttributeDefinitions,
      GlobalSecondaryIndexes: .Properties.GlobalSecondaryIndexes,
      BillingMode: &quot;PAY_PER_REQUEST&quot;
    }&#x27;</span> \
  | jq -c <span class="hljs-string">&#x27;.&#x27;</span> &gt; tables-seed.txt

<span class="hljs-comment"># Remove Null GSI if it exists since create-table will crash if a value is null</span>
sed -i <span class="hljs-string">&#x27;&#x27;</span> <span class="hljs-string">&#x27;s/,&quot;GlobalSecondaryIndexes&quot;:null//g&#x27;</span> tables-seed.txt

<span class="hljs-comment"># Run create table against each line in the seed data file</span>
<span class="hljs-keyword">while</span> <span class="hljs-built_in">read</span> -r line; <span class="hljs-keyword">do</span>
   aws dynamodb create-table --no-cli-pager --endpoint-url http://localhost:8000 --cli-input-json <span class="hljs-string">&quot;<span class="hljs-variable">$line</span>&quot;</span>
<span class="hljs-keyword">done</span> &lt; tables-seed.txt
</code></pre><p>My final pain point was SAMs built in <a class="externalLink" target="_blank" href="https://esbuild.github.io">ESBuild</a> integration. I <em><strong>love</strong></em> ESBuild. It&#39;s made my life 10x easier when working with Typescript. However, I was unable to get the built-in auto-build working when deploying.</p>
<p>This is how I bundle manually:</p>
<pre><code class="hljs language-bash">esbuild src/handler.ts \
  --target=es2020 \
  --platform=node \
  --external:aws-sdk \
  --sourcemap=linked \
  --outfile=.build/handler.js \
  --bundle
</code></pre><p>This is how SAM can do auto bundling:</p>
<pre><code class="hljs language-yaml"><span class="hljs-attr">Resources:</span>
  <span class="hljs-attr">HelloWorldFunction:</span>
    <span class="hljs-attr">Type:</span> <span class="hljs-string">AWS::Serverless::Function</span>
    <span class="hljs-attr">Properties:</span>
      <span class="hljs-comment"># ... Function properties</span>
    <span class="hljs-attr">Metadata:</span>
      <span class="hljs-attr">BuildMethod:</span> <span class="hljs-string">esbuild</span>
      <span class="hljs-attr">BuildProperties:</span>
        <span class="hljs-attr">Format:</span> <span class="hljs-string">esm</span>
        <span class="hljs-attr">Minify:</span> <span class="hljs-literal">false</span>
        <span class="hljs-attr">OutExtension:</span>
          <span class="hljs-bullet">-</span> <span class="hljs-string">.js=.mjs</span>
        <span class="hljs-attr">Target:</span> <span class="hljs-string">&quot;es2020&quot;</span>
        <span class="hljs-attr">Sourcemap:</span> <span class="hljs-literal">true</span>
        <span class="hljs-attr">External:</span>
          <span class="hljs-bullet">-</span> <span class="hljs-string">aws-sdk</span>
        <span class="hljs-attr">EntryPoints:</span>
          <span class="hljs-bullet">-</span> <span class="hljs-string">handler.ts</span>
</code></pre><p>I have no idea why, but in one of my projects, the SAM bundle is <code>21.25MB</code> while the manually bundled one is only <code>1.3MB</code>. The SAM bundle also doesn&#39;t work in a Lambda... Which is entirely the point. Luckily its relativity easy to use your own bundler, but you lose out on live reloading. I&#39;ll definitely come back to this one day, it&#39;s likely a poorly documented feature or there&#39;s something wrong with my config.</p>
<h3 id="aws-sam-ls">AWS SAM L's</h3>
<ul>
<li>Abstracted docker container management, not an issue until you want to use DynamoDB</li>
<li>DynamoDB is not handled out of the box with SAM</li>
<li>Cannot seed from a SAM template into a DynamoDB instance</li>
<li>Documentation &amp; Community support is lacking</li>
<li>Struggled to get an API gateway stood up<ul>
<li>This was confusing:<br><code>HttpMethod: &#39;*&#39;</code><br><code>Cors: &quot;&#39;*&#39;&quot;</code> (Quotes in quotes <strong>AND</strong> inconsistent!)</li>
</ul>
</li>
<li>Connectors seem cool but are an abstraction that makes things confusing and seem like they&#39;d become a nightmare to maintain, easier to write IAM roles.</li>
<li>Built in ESBuild just straight up didn&#39;t work when I used it, worked when I manually built with ESBuild.</li>
</ul>
<h3 id="aws-sam-ws">AWS SAM W's</h3>
<ul>
<li>Running API Gateway locally rocks</li>
<li>ESBuild if it worked would be <em>amazing</em></li>
<li>Not extremely abstracted from the underlying cloudformation</li>
<li>Easy to use existing tooling or own tools alongside, or in replacement of, SAMs tooling.</li>
</ul>
<h2 id="less-complaining">Less complaining!</h2>
<p>Even though I&#39;ve just spent the last while ranting about pain points in SAM, I can see its potential. It&#39;s almost there. I <strong>want</strong> to use it over the serverless framework. The main pain points I talked about were huge hurdles for me getting started which I only jumped over because I was <em>being paid to jump over them</em>. But, now that I know how to do it properly; I&#39;ll likely be using SAM for all my personally projects going forward.</p>
<p>The benefits of a serverless stack for me far outweigh the cons if I don&#39;t factor in the time learning how to properly use it. Serverless is amazing, and I do see it as the future of cloud computing, but the documentation and new developer journey is abysmal and extremely discouraging.</p>
<h2 id="recommendations-for-new-developers">Recommendations for new Developers</h2>
<ul>
<li>Use SAM, or plain Cloudformation.</li>
<li>Use plain IAM roles to define permissions:</li>
</ul>
<pre><code class="hljs language-yaml"><span class="hljs-attr">MyFunction:</span>
  <span class="hljs-attr">Type:</span> <span class="hljs-string">AWS::Serverless::Function</span>
  <span class="hljs-attr">Properties:</span>
    <span class="hljs-comment"># ...Properties</span>
    <span class="hljs-attr">Policies:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">Version:</span> <span class="hljs-string">&#x27;2012-10-17&#x27;</span>
        <span class="hljs-attr">Statement:</span>
          <span class="hljs-bullet">-</span> <span class="hljs-attr">Effect:</span> <span class="hljs-string">Allow</span>
            <span class="hljs-attr">Action:</span>
              <span class="hljs-bullet">-</span> <span class="hljs-string">dynamodb:Query</span>
              <span class="hljs-bullet">-</span> <span class="hljs-string">dynamodb:Scan</span>
              <span class="hljs-bullet">-</span> <span class="hljs-string">dynamodb:GetItem</span>
              <span class="hljs-bullet">-</span> <span class="hljs-string">dynamodb:PutItem</span>
              <span class="hljs-bullet">-</span> <span class="hljs-string">dynamodb:UpdateItem</span>
              <span class="hljs-bullet">-</span> <span class="hljs-string">dynamodb:DeleteItem</span>
              <span class="hljs-bullet">-</span> <span class="hljs-string">dynamodb:BatchGetItem</span>
            <span class="hljs-attr">Resource:</span>
              <span class="hljs-bullet">-</span> <span class="hljs-type">!Sub</span> <span class="hljs-string">${myTable.Arn}</span>
              <span class="hljs-bullet">-</span> <span class="hljs-type">!Sub</span> <span class="hljs-string">${myTable.Arn}/index/*</span>
</code></pre><ul>
<li>Include your stage name (prod/dev) in table names and names of other infrastructure items.</li>
<li>Learn how to properly take advantage of DynamoDB (I&#39;ll make a post about this one day!).<ul>
<li>Properly index your data.</li>
<li>Use multiple tables.</li>
<li>Be mindful of Read/Write units.</li>
</ul>
</li>
<li>If using SAM set up a custom API Gateway API, it&#39;s worth it for when you do eventually need it:</li>
</ul>
<pre><code class="hljs language-yaml"><span class="hljs-attr">ApiGatewayApi:</span>
  <span class="hljs-attr">Type:</span> <span class="hljs-string">AWS::Serverless::Api</span>
  <span class="hljs-attr">Properties:</span>
    <span class="hljs-attr">StageName:</span> <span class="hljs-type">!Ref</span> <span class="hljs-string">Stage</span>
    <span class="hljs-attr">Cors:</span> <span class="hljs-string">&quot;&#x27;*&#x27;&quot;</span>
    <span class="hljs-attr">MethodSettings:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">ResourcePath:</span> <span class="hljs-string">&quot;/*&quot;</span>
        <span class="hljs-attr">HttpMethod:</span> <span class="hljs-string">&quot;*&quot;</span>
        <span class="hljs-attr">ThrottlingRateLimit:</span> <span class="hljs-number">100</span>
        <span class="hljs-attr">ThrottlingBurstLimit:</span> <span class="hljs-number">500</span>
</code></pre><ul>
<li>Use Express &amp; Typescript with a single handler for most APIs:<sup><a id="footnote-ref-3" href="#footnote-3" data-footnote-ref aria-describedby="footnote-label">3</a></sup></li>
</ul>
<pre><code class="hljs language-ts"><span class="hljs-comment">/** handler.ts */</span>
<span class="hljs-keyword">import</span> express <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;express&#x27;</span>
<span class="hljs-keyword">import</span> serverless <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;serverless-http&#x27;</span>;

<span class="hljs-keyword">const</span> app = <span class="hljs-title function_">express</span>() <span class="hljs-comment">// Theres waaayyyyy more stuff here usualy.</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> handler = <span class="hljs-title function_">serverless</span>(app, {})

app.<span class="hljs-title function_">get</span>(<span class="hljs-string">&#x27;/my-function&#x27;</span>, <span class="hljs-function">(<span class="hljs-params">request, response</span>) =&gt;</span> {})
app.<span class="hljs-title function_">get</span>(<span class="hljs-string">&#x27;/my-function/sub&#x27;</span>, <span class="hljs-function">(<span class="hljs-params">request, response</span>) =&gt;</span> {})
</code></pre><pre><code class="hljs language-yaml"><span class="hljs-attr">MyFunction:</span>
  <span class="hljs-attr">Type:</span> <span class="hljs-string">AWS::Serverless::Function</span>
  <span class="hljs-attr">Properties:</span>
    <span class="hljs-comment"># ... Properties </span>
    <span class="hljs-attr">Events:</span>
      <span class="hljs-attr">Root:</span>
        <span class="hljs-attr">Type:</span> <span class="hljs-string">Api</span>
        <span class="hljs-attr">Properties:</span>
          <span class="hljs-attr">Path:</span> <span class="hljs-string">/my-function</span>
          <span class="hljs-attr">Method:</span> <span class="hljs-string">any</span>
          <span class="hljs-attr">RestApiId:</span>
            <span class="hljs-attr">Ref:</span> <span class="hljs-string">ApiGatewayApi</span>
      <span class="hljs-attr">Sub:</span>
        <span class="hljs-attr">Type:</span> <span class="hljs-string">Api</span>
        <span class="hljs-attr">Properties:</span>
          <span class="hljs-attr">Path:</span> <span class="hljs-string">/my-function/{any+}</span>
          <span class="hljs-attr">Method:</span> <span class="hljs-string">any</span>
          <span class="hljs-attr">RestApiId:</span>
            <span class="hljs-attr">Ref:</span> <span class="hljs-string">ApiGatewayApi</span>
</code></pre><ul>
<li>Ensure everything runs locally as well as once deployed!<ul>
<li>If you&#39;ve split your infrastructure into separate stacks it should be easy to deploy to a temporary testing stage.</li>
<li>If working with multiple developers it is a <strong>horrible</strong> idea to test by deploying to <code>dev</code> or <code>prod</code>.</li>
</ul>
</li>
<li>Set up GitHub actions, Bitbucket Pipelines etc. to deploy when pushing to <code>dev</code> or <code>prod</code>, keep it predictable!</li>
<li>If it seems too complicated it probably is. Serverless (sadly) currently has lots of ways of doing one thing. Research everything!</li>
</ul>
<section class="footnotes" data-footnotes>
<h2 id="footnote-label" class="sr-only">Footnotes</h2>
<ol>
<li id="footnote-1">
<p>We use macOS for development at the company I currently work for at the time of writing, we develop iOS applications which requires the use of XCode. <a href="#footnote-ref-1" data-footnote-backref aria-label="Back to reference 1">↩</a></p>
</li>
<li id="footnote-2">
<p><a class="externalLink" target="_blank" href="https://github.com/99x/serverless-dynamodb-local/issues/294">Major plugin became unmaintained and broke everyone's builds</a>. <a href="#footnote-ref-2" data-footnote-backref aria-label="Back to reference 2">↩</a></p>
</li>
<li id="footnote-3">
<p><a class="externalLink" target="_blank" href="https://github.com/pfych/web-mentions/blob/AWS-SAM-Migration/src/handler.ts">Example on my webmentions repo</a> has a single handler. <a href="#footnote-ref-3" data-footnote-backref aria-label="Back to reference 3">↩</a></p>
</li>
</ol>
</section>
 ]]></content>
                </entry><entry>
                  <id>https://pfy.ch/programming/projects/webmentions.html</id>
                  <title>Webmentions</title>
                  <published>2023-06-26T03:50:25.000Z</published>
                  <updated>2025-03-25T04:54:25.000Z</updated>
                  <link>https://pfy.ch/programming/projects/webmentions.html</link>
                  <emoji>💻</emoji>
                  <content type="html" xml:base="https://pfy.ch/programming/projects/webmentions.html"><![CDATA[ <div class="banner">

<p>I&#39;ve written a newer post about this <a class=""  href="../webmentions.html">here</a>. This article is still mostly relevant regarding the implementation, but the code is written using SAM - which I no-longer use. </p>
</div>

<p>I recently learned about the <a class="externalLink" target="_blank" href="https://www.w3.org/TR/webmention/">W3 Spec for Webmentions</a>! </p>
<p>This spec is described by W3 as:</p>
<blockquote>
<p>[...] a simple way to notify any URL when you mention it on your site. From the receiver&#39;s perspective, it&#39;s a way to request notifications when other sites mention it.</p>
</blockquote>
<p>I&#39;ve always wanted to write an implementation for a spec and this one did not seem extremely complex, <em>plus</em> I had a long weekend coming up, so I thought it&#39;d make for a fun challenge!</p>
<h2 id="the-plan">The Plan</h2>
<ol>
<li>Write a serverless implementation of webmentions.</li>
<li>It should use AWS Cloudformation to stand everything up automatically</li>
<li>It should be cheap to run indefinitely</li>
<li>It should be testable locally</li>
</ol>
<p>Breaking a receiver down, there are really only 3 endpoints required:</p>
<ol>
<li>Submit a webmention</li>
<li>Get the status of a webmention</li>
<li>Query webmentions</li>
</ol>
<p>The basic flow of our function to receive webmentions is as follows:</p>
<pre><code class="hljs language-mermaid">sequenceDiagram
    participant User
    participant Lambda
    participant DynamoDB
    
    User -&gt;&gt; Lambda: Send webmention request
    Lambda -&gt;&gt; Lambda: Confirm valid request
    Lambda -&gt;&gt; DynamoDB: Create status object
    DynamoDB -&gt;&gt; Lambda: Return status object identifier
    Lambda -&gt;&gt; User: Return 201 &amp; correct headers
    
    Lambda -&gt;&gt; Lambda: Confirm mention exists
    Lambda -&gt;&gt; DynamoDB: Create webmention object
    Lambda -&gt;&gt; DynamoDB: Update status object
    
    User -&gt;&gt; Lambda: Query webmention
    Lambda -&gt;&gt; DynamoDB: Request webmention
    DynamoDB -&gt;&gt; Lambda: Return webmention
    Lambda -&gt;&gt; User: Return webmention
</code></pre><h2 id="cloud-lambda-whats-the-cost">Cloud?! Lambda?! What's the cost?!</h2>
<p>I&#39;ve chosen using DynamoDB &amp; Lambda on AWS since when used correctly they&#39;re extremely cheap and efficient. Both are charged based on usage so if you&#39;re running a small site like mine you can expect the bill to come out at basically nothing. Lets do some quick math.</p>
<h3 id="dynamodb">DynamoDB</h3>
<p>DynamoDB charges based on read &amp; write units. With queries against an index costing 1 read unit and writes costing 1 unit per index on the table. The downside of Dynamo is that if you expect your data structure to be complex or have intricate relationships - it can end up costing much, <em>much</em>, more than a standard database.</p>
<p>Amazon currently has the cost of read and write units priced at the following:</p>
<table>
<thead>
<tr>
<th></th>
<th>Cost</th>
</tr>
</thead>
<tbody><tr>
<td>Write Unit</td>
<td>$1.4232 / Million</td>
</tr>
<tr>
<td>Read Unit</td>
<td>$0.2846 / Million</td>
</tr>
</tbody></table>
<p>Then if we break down how many units we use per function in our application: </p>
<table>
<thead>
<tr>
<th>Function</th>
<th>Read Units Used</th>
<th>Write Units Used</th>
</tr>
</thead>
<tbody><tr>
<td>Initial Request</td>
<td>0</td>
<td>1</td>
</tr>
<tr>
<td>Query Request Status</td>
<td>1</td>
<td>0</td>
</tr>
<tr>
<td>Create Webmention</td>
<td>0</td>
<td>2<sup><a id="footnote-ref-1" href="#footnote-1" data-footnote-ref aria-describedby="footnote-label">1</a></sup></td>
</tr>
<tr>
<td>Query URL for webmention</td>
<td>1</td>
<td>0</td>
</tr>
</tbody></table>
<p>We can calculate the cost as the following:</p>
<table>
<thead>
<tr>
<th>Function</th>
<th>Read units used</th>
<th>Write units used</th>
<th>Cost</th>
</tr>
</thead>
<tbody><tr>
<td>1000 Mentions<sup><a id="footnote-ref-2" href="#footnote-2" data-footnote-ref aria-describedby="footnote-label">2</a></sup></td>
<td>1000</td>
<td>3000</td>
<td>$0.0045542</td>
</tr>
<tr>
<td>1000 Page-views</td>
<td>1000</td>
<td>0</td>
<td>$0.0002846</td>
</tr>
</tbody></table>
<h3 id="lambda">Lambda</h3>
<p>AWS Lambda is a serverless compute platform where you can just <em>run code</em>. Amazon will provision servers globally for you, and these servers will only run while your function is active.</p>
<p>We run our lambda with 512mb of memory<sup><a id="footnote-ref-3" href="#footnote-3" data-footnote-ref aria-describedby="footnote-label">3</a></sup>, and with a timeout of 15 seconds. AWS charge per million requests and per second the function runs based on the memory used.</p>
<table>
<thead>
<tr>
<th></th>
<th>Cost</th>
</tr>
</thead>
<tbody><tr>
<td>Request</td>
<td>$0.20 / Million</td>
</tr>
<tr>
<td>Memory</td>
<td>$0.0000000083 / second</td>
</tr>
</tbody></table>
<p>Assuming worst case scenario and our function runs for 15 seconds we&#39;re paying <code>0.0000003245</code> per execution. In reality the cost is less than this, especially when querying for webmentions &amp; status objects since its runs at almost sub second speeds.</p>
<table>
<thead>
<tr>
<th></th>
<th>Requests made</th>
<th>Worst case exec</th>
<th>Cost</th>
</tr>
</thead>
<tbody><tr>
<td>Creating 1000 Mentions</td>
<td>1000</td>
<td>15 seconds</td>
<td>$0.00032449</td>
</tr>
<tr>
<td>Querying 1000 Mentions</td>
<td>1000</td>
<td>1 second</td>
<td>$0.0002083</td>
</tr>
</tbody></table>
<h3 id="total-stack-cost">Total stack cost</h3>
<p>Combining both our DynamoDB &amp; Lambda costs we can figure out at what point our bill with become &quot;excessive&quot; (more than a dollar).</p>
<table>
<thead>
<tr>
<th></th>
<th>Dynamo</th>
<th>Lambda</th>
<th>Total</th>
</tr>
</thead>
<tbody><tr>
<td>Creating 1000 Mentions</td>
<td>$0.00032449</td>
<td>$0.00032449</td>
<td>$0.00064898</td>
</tr>
<tr>
<td>Querying 1000 Mentions</td>
<td>$0.0002846</td>
<td>$0.0002083</td>
<td>$0.0004929</td>
</tr>
</tbody></table>
<p>This means that It&#39;ll take around 1,540,880 webmention <em><strong>creation</strong></em> requests before our bill exceeds a dollar! We can use AWS Budgets &amp; API Gateway Rate limiting to stop execution before we hit this, which I&#39;d highly recommend. Nothing is worse than an unexpected bill at the end of the month.</p>
<h1 id="implementation">Implementation</h1>
<p>Our final stack should consist of the following:</p>
<ul>
<li>1 Lambda: Running express with 3 entry points.</li>
<li>2 Tables: One with a single index and the other with two indexes.</li>
</ul>
<h2 id="tables">Tables</h2>
<p>The status table is extremely simple, since it only really needs to store basic information temporarily. It would be possible to add a TTL to records, however for now I plan to manually clean this out over time.</p>
<p>The table consists of a single index: <code>id</code>. Which will be used by other services to query the status of webmentions.</p>
<pre><code class="hljs language-yaml"><span class="hljs-attr">statusTable:</span>
  <span class="hljs-attr">Type:</span> <span class="hljs-string">AWS::DynamoDB::Table</span>
  <span class="hljs-attr">Properties:</span>
    <span class="hljs-attr">TableName:</span> <span class="hljs-string">webmentions-status-table</span> 
    <span class="hljs-attr">AttributeDefinitions:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">AttributeName:</span> <span class="hljs-string">id</span>
        <span class="hljs-attr">AttributeType:</span> <span class="hljs-string">S</span>
    <span class="hljs-attr">KeySchema:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">AttributeName:</span> <span class="hljs-string">id</span>
        <span class="hljs-attr">KeyType:</span> <span class="hljs-string">HASH</span>
    <span class="hljs-attr">BillingMode:</span> <span class="hljs-string">PAY_PER_REQUEST</span>
</code></pre><p>The mention table has a few extra definitions, most importantly it&#39;s target global secondary index (GSI).</p>
<p>We use <code>id</code> as the main unique index, however we do not query it. Instead, our functions query based on the webmention <code>target</code>. By indexing this field, we can get Webmentions for a page at incredibly fast speeds. </p>
<pre><code class="hljs language-yaml"><span class="hljs-attr">mentionTable:</span>
  <span class="hljs-attr">Type:</span> <span class="hljs-string">AWS::DynamoDB::Table</span>
  <span class="hljs-attr">Properties:</span>
    <span class="hljs-attr">TableName:</span> <span class="hljs-string">webmentions-mention-table</span> 
    <span class="hljs-attr">AttributeDefinitions:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">AttributeName:</span> <span class="hljs-string">id</span>
        <span class="hljs-attr">AttributeType:</span> <span class="hljs-string">S</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">AttributeName:</span> <span class="hljs-string">target</span>
        <span class="hljs-attr">AttributeType:</span> <span class="hljs-string">S</span>
    <span class="hljs-attr">KeySchema:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">AttributeName:</span> <span class="hljs-string">id</span>
        <span class="hljs-attr">KeyType:</span> <span class="hljs-string">HASH</span>
    <span class="hljs-attr">GlobalSecondaryIndexes:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">IndexName:</span> <span class="hljs-string">target-index</span>
        <span class="hljs-attr">KeySchema:</span>
          <span class="hljs-bullet">-</span> <span class="hljs-attr">AttributeName:</span> <span class="hljs-string">target</span>
            <span class="hljs-attr">KeyType:</span> <span class="hljs-string">HASH</span>
        <span class="hljs-attr">Projection:</span>
          <span class="hljs-attr">ProjectionType:</span> <span class="hljs-string">ALL</span>
    <span class="hljs-attr">BillingMode:</span> <span class="hljs-string">PAY_PER_REQUEST</span>
</code></pre><h2 id="lambda">Lambda</h2>
<p>We then define our Lambda &amp; its corresponding API Gateway.  This is pretty chunky since we also need to  give our function permission to access the tables we&#39;ve just defined.</p>
<pre><code class="hljs language-yaml">  <span class="hljs-attr">ApiGatewayApi:</span>
    <span class="hljs-attr">Type:</span> <span class="hljs-string">AWS::Serverless::Api</span>
    <span class="hljs-attr">Properties:</span>
      <span class="hljs-attr">StageName:</span> <span class="hljs-type">!Ref</span> <span class="hljs-string">Stage</span>
      <span class="hljs-attr">Cors:</span> <span class="hljs-string">&quot;&#x27;*&#x27;&quot;</span>
      <span class="hljs-attr">MethodSettings:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">ResourcePath:</span> <span class="hljs-string">&quot;/*&quot;</span>
          <span class="hljs-attr">HttpMethod:</span> <span class="hljs-string">&quot;*&quot;</span>
          <span class="hljs-attr">ThrottlingRateLimit:</span> <span class="hljs-number">100</span>
          <span class="hljs-attr">ThrottlingBurstLimit:</span> <span class="hljs-number">500</span>

  <span class="hljs-attr">Webmention:</span>
    <span class="hljs-attr">Type:</span> <span class="hljs-string">AWS::Serverless::Function</span>
    <span class="hljs-attr">Properties:</span>
      <span class="hljs-attr">MemorySize:</span> <span class="hljs-number">512</span>
      <span class="hljs-attr">Timeout:</span> <span class="hljs-number">15</span>
      <span class="hljs-attr">CodeUri:</span> <span class="hljs-string">.build</span>
      <span class="hljs-attr">Handler:</span> <span class="hljs-string">handler.handler</span>
      <span class="hljs-attr">Runtime:</span> <span class="hljs-string">nodejs14.x</span>
      <span class="hljs-attr">Architectures:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-string">x86_64</span>
      <span class="hljs-attr">Policies:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">Version:</span> <span class="hljs-string">&#x27;2012-10-17&#x27;</span>
          <span class="hljs-attr">Statement:</span>
            <span class="hljs-bullet">-</span> <span class="hljs-attr">Effect:</span> <span class="hljs-string">Allow</span>
              <span class="hljs-attr">Action:</span>
                <span class="hljs-bullet">-</span> <span class="hljs-string">dynamodb:Query</span>
                <span class="hljs-bullet">-</span> <span class="hljs-string">dynamodb:Scan</span>
                <span class="hljs-bullet">-</span> <span class="hljs-string">dynamodb:GetItem</span>
                <span class="hljs-bullet">-</span> <span class="hljs-string">dynamodb:PutItem</span>
                <span class="hljs-bullet">-</span> <span class="hljs-string">dynamodb:UpdateItem</span>
                <span class="hljs-bullet">-</span> <span class="hljs-string">dynamodb:DeleteItem</span>
                <span class="hljs-bullet">-</span> <span class="hljs-string">dynamodb:BatchGetItem</span>
              <span class="hljs-attr">Resource:</span>
                <span class="hljs-bullet">-</span> <span class="hljs-type">!Sub</span> <span class="hljs-string">${statusTable.Arn}</span>
                <span class="hljs-bullet">-</span> <span class="hljs-type">!Sub</span> <span class="hljs-string">${statusTable.Arn}/index/*</span>
                <span class="hljs-bullet">-</span> <span class="hljs-type">!Sub</span> <span class="hljs-string">${mentionTable.Arn}</span>
                <span class="hljs-bullet">-</span> <span class="hljs-type">!Sub</span> <span class="hljs-string">${mentionTable.Arn}/index/*</span>
      <span class="hljs-attr">Events:</span>
        <span class="hljs-attr">Root:</span>
          <span class="hljs-attr">Type:</span> <span class="hljs-string">Api</span>
          <span class="hljs-attr">Properties:</span>
            <span class="hljs-attr">Path:</span> <span class="hljs-string">/web-mentions</span>
            <span class="hljs-attr">Method:</span> <span class="hljs-string">any</span>
            <span class="hljs-attr">RestApiId:</span>
              <span class="hljs-attr">Ref:</span> <span class="hljs-string">ApiGatewayApi</span>
        <span class="hljs-attr">Sub:</span>
          <span class="hljs-attr">Type:</span> <span class="hljs-string">Api</span>
          <span class="hljs-attr">Properties:</span>
            <span class="hljs-attr">Path:</span> <span class="hljs-string">/web-mentions/{any+}</span>
            <span class="hljs-attr">Method:</span> <span class="hljs-string">any</span>
            <span class="hljs-attr">RestApiId:</span>
              <span class="hljs-attr">Ref:</span> <span class="hljs-string">ApiGatewayApi</span>
</code></pre><p>A minor complaint with AWS SAM is that it&#39;s built in <code>esbuild</code> bundler for functions does not behave as expected, to get around this we manually bundle ourselves. </p>
<p>Luckily <code>esbuild</code> works pretty much on its own and doesn&#39;t require very much external config like webpack or other bundlers.</p>
<pre><code class="hljs language-bash">esbuild src/handler.ts \
  --target=es2020 \
  --platform=node \
  --external:aws-sdk \
  --sourcemap=linked \
  --outfile=.build/handler.js \
  --bundle
</code></pre><h2 id="express">Express</h2>
<p>Getting express to work on a Lambda is super easy once you know what you&#39;re doing<sup><a id="footnote-ref-4" href="#footnote-4" data-footnote-ref aria-describedby="footnote-label">4</a></sup>. </p>
<p>We&#39;ve set up our API Gateway to forward requests at <code>/web-metions</code> or <code>/web-mentions/{any+}</code> through to our Lambda.</p>
<pre><code class="hljs language-ts"><span class="hljs-keyword">import</span> express, { <span class="hljs-title class_">Express</span>, <span class="hljs-title class_">NextFunction</span>, <span class="hljs-title class_">Request</span>, <span class="hljs-title class_">Response</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;express&#x27;</span>;
<span class="hljs-keyword">import</span> {
  <span class="hljs-title class_">APIGatewayEventRequestContext</span>,
  <span class="hljs-title class_">APIGatewayProxyEvent</span>,
} <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;aws-lambda&#x27;</span>;
<span class="hljs-keyword">import</span> serverless <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;serverless-http&#x27;</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">type</span> <span class="hljs-title class_">RequestContext</span> = <span class="hljs-title class_">Request</span> &amp; {
  <span class="hljs-attr">context</span>: <span class="hljs-title class_">APIGatewayEventRequestContext</span>;
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> createApp = (): <span class="hljs-function"><span class="hljs-params">Express</span> =&gt;</span> {
  <span class="hljs-keyword">const</span> app = <span class="hljs-title function_">express</span>();
  app.<span class="hljs-title function_">use</span>(express.<span class="hljs-title function_">urlencoded</span>({ <span class="hljs-attr">extended</span>: <span class="hljs-literal">true</span> }));
  app.<span class="hljs-title function_">use</span>(express.<span class="hljs-title function_">json</span>());
  app.<span class="hljs-title function_">use</span>(<span class="hljs-title function_">cors</span>(corsOptions));
  app.<span class="hljs-title function_">options</span>(<span class="hljs-string">&#x27;*&#x27;</span>, <span class="hljs-title function_">cors</span>(corsOptions));
  app.<span class="hljs-title function_">use</span>(<span class="hljs-function">(<span class="hljs-params"><span class="hljs-attr">req</span>: <span class="hljs-title class_">RequestContext</span>, <span class="hljs-attr">res</span>: <span class="hljs-title class_">Response</span>, <span class="hljs-attr">next</span>: <span class="hljs-title class_">NextFunction</span></span>) =&gt;</span> {
    <span class="hljs-variable language_">console</span>.<span class="hljs-title function_">log</span>(<span class="hljs-string">`Request: <span class="hljs-subst">${req.method}</span> <span class="hljs-subst">${req.originalUrl}</span>`</span>);
    <span class="hljs-title function_">next</span>();
  });

  <span class="hljs-keyword">return</span> app;
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> <span class="hljs-title function_">createHandler</span> = (<span class="hljs-params"><span class="hljs-attr">app</span>: <span class="hljs-title class_">Express</span></span>) =&gt;
  <span class="hljs-title function_">serverless</span>(app, {
    <span class="hljs-title function_">request</span>(<span class="hljs-params"><span class="hljs-attr">request</span>: <span class="hljs-title class_">RequestContext</span>, <span class="hljs-attr">event</span>: <span class="hljs-title class_">APIGatewayProxyEvent</span></span>) {
      request.<span class="hljs-property">context</span> = event.<span class="hljs-property">requestContext</span>;
    },
  });

<span class="hljs-keyword">const</span> app = <span class="hljs-title function_">createApp</span>();
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> handler = <span class="hljs-title function_">createHandler</span>(app);
</code></pre><p>Once we&#39;ve configured the app we can now use express as we normally would!</p>
<pre><code class="hljs language-ts">app.<span class="hljs-title function_">post</span>(
  <span class="hljs-string">&#x27;/web-mentions&#x27;</span>,
  <span class="hljs-title function_">async</span> (<span class="hljs-attr">request</span>: <span class="hljs-title class_">RequestContext</span>, <span class="hljs-attr">response</span>: <span class="hljs-title class_">Response</span>) =&gt; {},
);

app.<span class="hljs-title function_">get</span>(
  <span class="hljs-string">&#x27;/web-mentions/status/:id&#x27;</span>,
  <span class="hljs-title function_">async</span> (<span class="hljs-attr">request</span>: <span class="hljs-title class_">RequestContext</span>, <span class="hljs-attr">response</span>: <span class="hljs-title class_">Response</span>) =&gt; {},
);

app.<span class="hljs-title function_">post</span>(
  <span class="hljs-string">&#x27;/web-mentions/query&#x27;</span>,
  <span class="hljs-title function_">async</span> (<span class="hljs-attr">request</span>: <span class="hljs-title class_">RequestContext</span>, <span class="hljs-attr">response</span>: <span class="hljs-title class_">Response</span>) =&gt; {},
);
</code></pre><h1 id="deploying">Deploying</h1>
<p>AWS SAM provides single command deploy, I&#39;ve wrapped both build and deploy up into scripts in the projects <code>package.json</code>.</p>
<pre><code class="hljs language-bash"><span class="hljs-built_in">export</span> REGION=<span class="hljs-string">&#x27;ap-southeast-2&#x27;</span> <span class="hljs-comment"># Our target AWS region</span>
<span class="hljs-built_in">export</span> PROFILE=<span class="hljs-string">&#x27;pfych-aws&#x27;</span> <span class="hljs-comment"># Our target AWS profile as configured in AWS-CLI</span>
npm run build
npm run deploy:dev
</code></pre><p>This process takes a few minutes on first deploy but should be sub minute on any subsequent deploys as long as you don&#39;t change the SAM Template.</p>
<h1 id="in-closing">In closing</h1>
<p>It was a fun challenge to implement Webmentions in a serverless manner. It wasn&#39;t extremely difficult since the spec is well-defined. I did skip over web mention updates &amp; deletions, but I&#39;ll implement them at another time. Serverless definitely has warts &amp; almost all of it is in its tooling &amp; documentation. I hope that with time this can improve.</p>
<p>My next steps is to implement sending Webmentions from my CMS when I make new posts, but I haven&#39;t had too much time to work on personal projects recently.</p>
<p>The project is <a class="externalLink" target="_blank" href="https://github.com/pfych/web-mentions/tree/AWS-SAM-Migration">available on Github</a>. It currently is deployed with the Serverless Framework but there is a pending PR to use AWS SAM instead since it has less 3rd party dependencies.</p>
<section class="footnotes" data-footnotes>
<h2 id="footnote-label" class="sr-only">Footnotes</h2>
<ol>
<li id="footnote-1">
<p>We have 2 indexes, so two write units are consumed  <a href="#footnote-ref-1" data-footnote-backref aria-label="Back to reference 1">↩</a></p>
</li>
<li id="footnote-2">
<p>Assuming the request status is queried once <a href="#footnote-ref-2" data-footnote-backref aria-label="Back to reference 2">↩</a></p>
</li>
<li id="footnote-3">
<p>We could potentially run this with much less memory if we wanted too <a href="#footnote-ref-3" data-footnote-backref aria-label="Back to reference 3">↩</a></p>
</li>
<li id="footnote-4">
<p>Classic case of nothing being documented well 🥲 <a href="#footnote-ref-4" data-footnote-backref aria-label="Back to reference 4">↩</a></p>
</li>
</ol>
</section>
 ]]></content>
                </entry><entry>
                  <id>https://pfy.ch/programming/bash.html</id>
                  <title>bash</title>
                  <published>2023-06-08T03:50:25.000Z</published>
                  <updated>2024-08-22T04:56:04.000Z</updated>
                  <link>https://pfy.ch/programming/bash.html</link>
                  <emoji>💻</emoji>
                  <content type="html" xml:base="https://pfy.ch/programming/bash.html"><![CDATA[ <banner-element>

<p>This article was written as a supplement to a small talk I gave in the office where I work about scripting. It sort of stands on its own but there will be references to internal practices and tools which might not make sense to some readers.</p>
</banner-element>

<h2 id="preface">Preface</h2>
<p>We constantly refer to &quot;Bash&quot; around the office in reference to our scripts but what actually is Bash and can we use it more effectively?</p>
<p>Bash is a Unix Shell, that runs inside a Terminal instance.</p>
<pre><code class="hljs language-mermaid">flowchart LR
    Computer --&gt; Terminal
    Terminal --&gt; Shell[Unix Shell]
    Shell --&gt; Kernel[&quot;Kernel (Operating System)&quot;]
</code></pre><p>Your Terminal is a piece of software on your computer that can interact with your selected Unix Shell. Some of the common terminal tools we see and use are:</p>
<ul>
<li>Terminal.app</li>
<li>iTerm2</li>
<li>The terminal in our IDEs</li>
</ul>
<p>The Terminal program handles input, output and errors between you and your shell. </p>
<p>The most common shell is Bash which is shorthand for its full name, &quot;<strong>B</strong>ourne <strong>A</strong>gain <strong>sh</strong>ell&quot;. <em>Bourne Again</em> since it&#39;s intended to be used as a replacement for the Bourne Shell (<code>sh</code>)<sup><a id="footnote-ref-1" href="#footnote-1" data-footnote-ref aria-describedby="footnote-label">1</a></sup>. Bash can run scripts written for the Bourne Shell but not vice versa</p>
<p>MacOS no longer ships with Bash as the default shell and instead uses ZSH. ZSH can run Bourne Shell scripts, but it cannot run Bash scripts.</p>
<p>So there are 3 main Unix Shells we interact with:</p>
<ul>
<li>Bourne Shell (often just called shell or <code>sh</code>)</li>
<li>Bash (Technically GNU Bash)</li>
<li>ZSH</li>
</ul>
<banner-element>

<p>From here on out I&#39;ll be refering to Bourne Shell scripts as &quot;Shell Scripts&quot;, Bash scripts as &quot;Bash Scripts&quot; and ZSH scripts as &quot;ZSH scripts&quot;</p>
</banner-element>

<p>So which shell do our scripts use?! </p>
<p>All scripts we use are prepended with the file extension <code>.sh</code>, however this file extension is <strong><em>not</em></strong> what is used to determine the shell!</p>
<p>Enter, the shebang!</p>
<pre><code class="hljs language-bash"><span class="hljs-meta">#!/usr/bin/env bash</span>
</code></pre><p>This line sits at the top of all of our scripts and tells our Terminal program what Unix Shell to run the script against.</p>
<p>Most Unix old heads will push for you to use either:</p>
<ul>
<li><code>#!/bin/sh</code></li>
<li><code>#!/usr/bin/env sh</code></li>
</ul>
<p>Since it&#39;s the most widely supported and come pre-installed on most unix/unix-like systems: MacOS, BSD, GNU Linux, etc.</p>
<p>MacOS &amp; BSD do not ship with most GNU utils. MacOS ships with GNU Bash, but not anything else. causing confusion when using tools like <code>sed</code> or <code>date</code> which have BSD and GNU versions.</p>
<p>The shebang tells your Terminal what program to run the following file under. This can be anything as long as it&#39;s a valid path to a binary.</p>
<h2 id="syntax-examples">Syntax Examples</h2>
<p>The following are some handy snippets that help explain the Syntax of Shell Scripts. Bash, ZSH and Shell all have extremely similar syntax. There are no types, and everything is considered &quot;text&quot;.</p>
<p>Binaries on your system can pass data via <code>stdin</code> and <code>stdout</code>, stdin can be referenced in your scripts with numbered variables:</p>
<pre><code class="hljs language-bash"><span class="hljs-meta">#!/usr/bin/env bash</span>
<span class="hljs-built_in">echo</span> <span class="hljs-string">&quot;<span class="hljs-variable">$1</span>&quot;</span>
</code></pre><pre><code class="hljs">&gt; ./example.sh &quot;hello world&quot;

hello world
</code></pre><p>a space is considered a separator, ensure you use quotes or escape the space if your input contains a space.</p>
<pre><code class="hljs">&gt; ./example.sh hello world

hello 
</code></pre><p>All 3 shells offer operators for moving your binaries <code>stdout</code> into the <code>stdin</code> of another binary.</p>
<h3 id="pipe-">Pipe (`|`)</h3>
<p>The Pipe (<code>|</code>) operator directly connects the output of one program to the input another</p>
<h3 id="arrow-brace-">Arrow Brace (`>`)</h3>
<p>The Arrow Brace (<code>&gt;</code>) writes the output of a binary to file</p>
<pre><code class="hljs">&gt; echo &quot;hello&quot; &gt; ./world.txt
&gt; cat ./world.txt

hello
</code></pre><h3 id="double-arrow-brace-">Double Arrow Brace (`>>`)</h3>
<p>The Double Arrow Brace (<code>&gt;&gt;</code>) appends the output of a binary to file</p>
<pre><code class="hljs">&gt; echo &quot;hello&quot; &gt; ./world.txt
&gt; cat ./world.txt

hello
hello
</code></pre><h3 id="subshells-">Subshells `$()`</h3>
<p>Subshells fork the current process and runs the inner content inside another process, it does not have access to any of its parents environment.<br>The <code>stdout</code> of the subshell is &quot;returned&quot;.</p>
<pre><code class="hljs language-bash">MY_VAR=$(<span class="hljs-built_in">echo</span> <span class="hljs-string">&quot;Hello World&quot;</span>)
<span class="hljs-built_in">echo</span> <span class="hljs-string">&quot;<span class="hljs-variable">$MY_VAR</span>&quot;</span>
</code></pre><h3 id="variable-expansion-">Variable Expansion `${}`</h3>
<p>This one&#39;s a bit complex, it allows operations to be run against a variable <em>before</em> the line is executed.</p>
<pre><code class="hljs language-bash">&gt; MY_VAR=<span class="hljs-string">&quot;&quot;</span>
&gt; <span class="hljs-built_in">echo</span> <span class="hljs-variable">${MY_VAR:-&quot;Missing!&quot;}</span>

Missing!
</code></pre><p>We use the <code>:-</code> operator here to substitute a default value if the left hand is missing. This is executed before the full line (with <code>echo</code>) is run.</p>
<p>There are a few operators:</p>
<ul>
<li><code>:-</code> Substitution with a default value</li>
<li><code>:=</code> Substitution with default assignment</li>
<li><code>:+</code> Substitution for actual value</li>
<li><code>:?</code> Substitution with value check</li>
</ul>
<p>These are explained in further detail in other guides<sup><a id="footnote-ref-2" href="#footnote-2" data-footnote-ref aria-describedby="footnote-label">2</a></sup>.</p>
<h3 id="single-quotes-">Single Quotes (`''`)</h3>
<p>This represents a plain string, you cannot use variables inside these.</p>
<pre><code class="hljs language-bash">&gt; MY_VAR=<span class="hljs-string">&#x27;HELLO WORLD&#x27;</span>
&gt; <span class="hljs-built_in">echo</span> <span class="hljs-string">&#x27;$MY_VAR&#x27;</span>

<span class="hljs-variable">$MY_VAR</span>
</code></pre><h3 id="double-quotes-">Double quotes (`""`)</h3>
<p>Fancy pants string, you can use variables inside them.</p>
<pre><code class="hljs language-bash">&gt; MY_VAR=<span class="hljs-string">&quot;HELLO WORLD&quot;</span>
&gt; <span class="hljs-built_in">echo</span> <span class="hljs-string">&quot;<span class="hljs-variable">$MY_VAR</span>&quot;</span>

HELLO WORLD
</code></pre><p>You should always wrap variables in double quotes when using them as input to a binary. </p>
<pre><code class="hljs language-bash">&gt; MY_VAR=<span class="hljs-string">&quot;HELLO WORLD&quot;</span>
&gt; <span class="hljs-built_in">echo</span> <span class="hljs-variable">$MY_VAR</span>

HELLO
Error: unknown <span class="hljs-built_in">command</span> WORLD
</code></pre><h2 id="useful-binaries-builtins">Useful binaries & builtins</h2>
<p>A lot of Unix-like systems ship with these tools, however be weary of slight syntactical differences between the BSD/GNU versions of these tools.</p>
<h3 id="cat">cat</h3>
<p><code>cat</code> is commonly used to output the contents of a file</p>
<pre><code class="hljs language-bash">&gt; <span class="hljs-built_in">cat</span> hello.txt

Hello world!
This is a second line
and a third line
</code></pre><h3 id="grep">grep</h3>
<p><code>grep</code> lets you easily search and filter stdout or files.</p>
<pre><code class="hljs language-bash">&gt; <span class="hljs-built_in">cat</span> hello.txt | grep <span class="hljs-string">&quot;world&quot;</span>

Hello world!
</code></pre><pre><code class="hljs language-bash">&gt; grep <span class="hljs-string">&quot;world&quot;</span> -f hello.txt

Hello world!
</code></pre><p>You can also pass a Regular Expression to grep. Linux Grep &amp; macOS Grep have minor differences in how they handle Regex, it&#39;s almost never an issue, but it&#39;s worth keeping in mind. (<code>-o</code> flag)</p>
<pre><code class="hljs language-bash"><span class="hljs-built_in">cat</span> .zsh_history | grep -E <span class="hljs-string">&#x27;[A-z]([0-9]*)&#x27;</span>
</code></pre><h3 id="for">for</h3>
<p><code>for</code> allows for easy iteration over files or arrays.</p>
<pre><code class="hljs language-bash"><span class="hljs-keyword">for</span> file <span class="hljs-keyword">in</span> *.png; <span class="hljs-keyword">do</span>
  <span class="hljs-built_in">echo</span> <span class="hljs-string">&quot;<span class="hljs-variable">$file</span>&quot;</span>
<span class="hljs-keyword">done</span>;
</code></pre><p>Bash &amp; ZSH have support for arrays:</p>
<pre><code class="hljs language-bash"><span class="hljs-built_in">declare</span> -a ARRAY_VAR=(<span class="hljs-string">&quot;foo&quot;</span> <span class="hljs-string">&quot;bar&quot;</span> <span class="hljs-string">&quot;baz&quot;</span>) 

<span class="hljs-keyword">for</span> variable <span class="hljs-keyword">in</span> <span class="hljs-string">&quot;<span class="hljs-variable">${ARRAY_VAR[@]}</span>&quot;</span>; <span class="hljs-keyword">do</span>
  <span class="hljs-built_in">echo</span> <span class="hljs-string">&quot;<span class="hljs-variable">$variable</span>&quot;</span>
<span class="hljs-keyword">done</span>
</code></pre><h3 id="if">if</h3>
<p><code>if</code> has different syntax than expected depending on your shell.</p>
<h4 id="bourne-shell-shbashzsh">Bourne Shell (SH/BASH/ZSH)</h4>
<p>You can pass flags to <code>if</code> to handle comparisons<sup><a id="footnote-ref-3" href="#footnote-3" data-footnote-ref aria-describedby="footnote-label">3</a></sup>.</p>
<pre><code class="hljs language-bash">A=<span class="hljs-string">&quot;FOO&quot;</span>
B=<span class="hljs-string">&quot;BAR&quot;</span>

<span class="hljs-keyword">if</span> <span class="hljs-variable">$A</span> -eq <span class="hljs-variable">$B</span>; <span class="hljs-keyword">then</span>
  <span class="hljs-built_in">echo</span> <span class="hljs-string">&quot;A is equal to B&quot;</span>
<span class="hljs-keyword">elif</span> <span class="hljs-variable">$A</span> -ne <span class="hljs-variable">$B</span>; <span class="hljs-keyword">then</span>
  <span class="hljs-built_in">echo</span> <span class="hljs-string">&quot;A is not equal to B&quot;</span>
<span class="hljs-keyword">done</span>
</code></pre><h4 id="bash-zsh">Bash & ZSH</h4>
<p>Bash 2.0.2 introduced the <code>[[]]</code> &quot;extend test command&quot; which allows for more standard syntax when doing comparisons.</p>
<pre><code class="hljs language-bash">A=<span class="hljs-string">&quot;FOO&quot;</span>
B=<span class="hljs-string">&quot;BAR&quot;</span>

<span class="hljs-keyword">if</span> [[ <span class="hljs-string">&quot;<span class="hljs-variable">$A</span>&quot;</span> == <span class="hljs-string">&quot;<span class="hljs-variable">$B</span>&quot;</span> ]]; <span class="hljs-keyword">then</span>
  <span class="hljs-built_in">echo</span> <span class="hljs-string">&quot;A is equal to B&quot;</span>
<span class="hljs-keyword">elif</span> [[ <span class="hljs-string">&quot;<span class="hljs-variable">$A</span>&quot;</span> != <span class="hljs-string">&quot;<span class="hljs-variable">$B</span>&quot;</span> ]]; <span class="hljs-keyword">then</span>
  <span class="hljs-built_in">echo</span> <span class="hljs-string">&quot;A is not equal to B&quot;</span>
<span class="hljs-keyword">done</span>
</code></pre><p>You can also use the original flags inside the square brackets:</p>
<pre><code class="hljs language-bash">A=<span class="hljs-string">&quot;FOO&quot;</span>
B=<span class="hljs-string">&quot;BAR&quot;</span>

<span class="hljs-keyword">if</span> [[ <span class="hljs-string">&quot;<span class="hljs-variable">$A</span>&quot;</span> -eq <span class="hljs-string">&quot;<span class="hljs-variable">$B</span>&quot;</span> ]]; <span class="hljs-keyword">then</span>
  <span class="hljs-built_in">echo</span> <span class="hljs-string">&quot;A is equal to B&quot;</span>
<span class="hljs-keyword">elif</span> [[ <span class="hljs-string">&quot;<span class="hljs-variable">$A</span>&quot;</span> -ne <span class="hljs-string">&quot;<span class="hljs-variable">$B</span>&quot;</span> ]]; <span class="hljs-keyword">then</span>
  <span class="hljs-built_in">echo</span> <span class="hljs-string">&quot;A is not equal to B&quot;</span>
<span class="hljs-keyword">done</span>
</code></pre><h3 id="sed">Sed</h3>
<p>Sed allows you to edit input or a file with a regular expression. Sed also has differences across Unix-like systems<sup><a id="footnote-ref-4" href="#footnote-4" data-footnote-ref aria-describedby="footnote-label">4</a></sup>.</p>
<pre><code class="hljs language-bash"><span class="hljs-built_in">echo</span> <span class="hljs-string">&quot;Hello world&quot;</span> | sed <span class="hljs-string">&#x27;s/world/mars/g&#x27;</span>
</code></pre><p>On BSD sed you need to supply <code>-i</code> when editing a file, this is not mandatory on GNU sed.</p>
<pre><code class="hljs language-bash">sed -i .bak -e <span class="hljs-string">&quot;s/world/mars/g&quot;</span> hello.txt
</code></pre><h3 id="jq">JQ</h3>
<p>JQ is not a built-in tool, but we use it often enough that it&#39;s worth mentioning. JQ allows you to parse JSON objects and offers almost all the utilities JS offers for working with Objects: <code>find</code>, <code>map</code>, <code>reduce</code>, <code>filter</code> etc. It&#39;s quite extensive and worth reading their docs<sup><a id="footnote-ref-5" href="#footnote-5" data-footnote-ref aria-describedby="footnote-label">5</a></sup> about its capabilities.</p>
<section class="footnotes" data-footnotes>
<h2 id="footnote-label" class="sr-only">Footnotes</h2>
<ol>
<li id="footnote-1">
<p><a class="externalLink" target="_blank" href="https://web.archive.org/web/20211228023030/https://groups.google.com/g/comp.unix.questions/c/iNjWwkyroR8/m/yedr9yDWSuQJ">Usenet Archive</a>. <a href="#footnote-ref-1" data-footnote-backref aria-label="Back to reference 1">↩</a></p>
</li>
<li id="footnote-2">
<p><a class="externalLink" target="_blank" href="https://en.wikibooks.org/wiki/Bourne_Shell_Scripting/Variable_Expansion">Wikibooks Tutorial (Bourne Shell)</a>. <a href="#footnote-ref-2" data-footnote-backref aria-label="Back to reference 2">↩</a></p>
</li>
<li id="footnote-3">
<p><a class="externalLink" target="_blank" href="https://en.wikibooks.org/wiki/Bourne_Shell_Scripting/Control_flow#Control_Flow">Control Flow in SH</a>. <a href="#footnote-ref-3" data-footnote-backref aria-label="Back to reference 3">↩</a></p>
</li>
<li id="footnote-4">
<p><a class="externalLink" target="_blank" href="https://unix.stackexchange.com/questions/92895/how-can-i-achieve-portability-with-sed-i-in-place-editing">Sed Portability</a>. <a href="#footnote-ref-4" data-footnote-backref aria-label="Back to reference 4">↩</a></p>
</li>
<li id="footnote-5">
<p><a class="externalLink" target="_blank" href="https://jqlang.github.io/jq/manual/#Basicfilters">JQ Basic filters</a>. <a href="#footnote-ref-5" data-footnote-backref aria-label="Back to reference 5">↩</a></p>
</li>
</ol>
</section>
 ]]></content>
                </entry><entry>
                  <id>https://pfy.ch/photography/urbex/assorted/tokyo-2023.html</id>
                  <title>Tokyo 2023</title>
                  <published>2023-05-14T14:00:00.000Z</published>
                  <updated>2025-01-07T05:31:27.000Z</updated>
                  <link>https://pfy.ch/photography/urbex/assorted/tokyo-2023.html</link>
                  <emoji>✈️</emoji>
                  <content type="html" xml:base="https://pfy.ch/photography/urbex/assorted/tokyo-2023.html"><![CDATA[ <p>At the beginning of May I took a 2 week (much needed) holiday overseas to Tokyo. I made zero plans, spent most of my time in the Arcades and caught up with some friends. I&#39;m already planning my next trip.</p>
<p>I spent maybe 75% of my time there split between GiGO, LeisureLand and Hey in Akihabara playing games ranging from Dodonpachi &amp; IIDX to games like Kancolle Arcade (I have a new  addiction). </p>
<p>While some people might see this as a waste of a trip I really enjoyed it. Australia doesn&#39;t have very many good arcades and the good arcades lack a lot of popular games. It was amazing to be able to play without worrying about a queue of people behind you waiting to play since there were so many cabinets.</p>
<h1 id="the-imperial-palace-greenery">The Imperial Palace & Greenery</h1>
<p>On my first day in Tokyo I decided to wander around the centre of the city and see what&#39;s around. I later learned that I had accidentally lined my trip up with Golden Week, which explained why everywhere was insanely busy. I wasn&#39;t able to get into the Palace, so I mostly took photos of the outside.</p>
<p><img src="https://assets.pfy.ch/md/000005600015.jpg" alt="" />
<img src="https://assets.pfy.ch/md/000005600016.jpg" alt="" />
<img src="https://assets.pfy.ch/md/000005600017.jpg" alt="" />
<img src="https://assets.pfy.ch/md/000005600018.jpg" alt="" />
<img src="https://assets.pfy.ch/md/000005600019.jpg" alt="" />
<img src="https://assets.pfy.ch/md/000005600020.jpg" alt="" />
<img src="https://assets.pfy.ch/md/000005600021.jpg" alt="" />
<img src="https://assets.pfy.ch/md/000005600022.jpg" alt="" />
<img src="https://assets.pfy.ch/md/000005600023.jpg" alt="" />
<img src="https://assets.pfy.ch/md/000005600024.jpg" alt="" />
<img src="https://assets.pfy.ch/md/000005600025.jpg" alt="" />
<img src="https://assets.pfy.ch/md/000005600011.jpg" alt="" />
<img src="https://assets.pfy.ch/md/000005600012.jpg" alt="" />
<img src="https://assets.pfy.ch/md/000005600014.jpg" alt="" /></p>
<h1 id="day-out-with-friends">Day out with friends</h1>
<p>I linked up with a friend who happened to be traveling around Japan at the same time I was in Tokyo. We went out to the Science Museum, Skytree and the Aquarium. While it was crazy busy at all the touristy destinations, it was really fun. </p>
<p><img src="https://assets.pfy.ch/md/000005600001.jpg" alt="" />
<img src="https://assets.pfy.ch/md/000005600002.jpg" alt="" />
<img src="https://assets.pfy.ch/md/000005600003.jpg" alt="" />
<img src="https://assets.pfy.ch/md/000005600004.jpg" alt="" />
<img src="https://assets.pfy.ch/md/000005600005.jpg" alt="" />
<img src="https://assets.pfy.ch/md/000005600010.jpg" alt="" />
<img src="https://assets.pfy.ch/md/000005610010.jpg" alt="" />
<img src="https://assets.pfy.ch/md/000005610011.jpg" alt="" />
<img src="https://assets.pfy.ch/md/000005610012.jpg" alt="" />
<img src="https://assets.pfy.ch/md/000005610014.jpg" alt="" />
<img src="https://assets.pfy.ch/md/000005610015.jpg" alt="" />
<img src="https://assets.pfy.ch/md/000005610017.jpg" alt="" />
<img src="https://assets.pfy.ch/md/000005610018.jpg" alt="" />
<img src="https://assets.pfy.ch/md/000005610019.jpg" alt="" /></p>
<h1 id="asakusa">Asakusa</h1>
<p>I had a day by myself and didn&#39;t feel like spending another whole day in Akihabara playing games. I caught the train up to Asakusa to see what was around.</p>
<p><img src="https://assets.pfy.ch/md/000005610020.jpg" alt="" />
<img src="https://assets.pfy.ch/md/000005610021.jpg" alt="" />
<img src="https://assets.pfy.ch/md/000005610022.jpg" alt="" />
<img src="https://assets.pfy.ch/md/000005610024.jpg" alt="" />
<img src="https://assets.pfy.ch/md/000005610025.jpg" alt="" />
<img src="https://assets.pfy.ch/md/000005610026.jpg" alt="" /></p>
<h1 id="late-nights-in-kameido-ikebukuro">Late nights in Kameido & Ikebukuro</h1>
<p>My Hotel was in Kameido which was really close to Akihabara, so I&#39;d always come back super late at night. I cant remember which of these photos are from Kameido and which ones are from Ikebukuro since I had loaded this camera with CineStill 800T specifically for taking photos at night, and I was relatively sporadic with when I&#39;d decide to take photos. </p>
<p>I&#39;d never heard about Ikebukuro but some friends I met up with live nearby and said the Game Centres in the area were chill so I spent a bit of time up there.</p>
<p><img src="https://assets.pfy.ch/md/000005590007.jpg" alt="" />
<img src="https://assets.pfy.ch/md/000005590010.jpg" alt="" />
<img src="https://assets.pfy.ch/md/000005590011.jpg" alt="" />
<img src="https://assets.pfy.ch/md/000005590012.jpg" alt="" />
<img src="https://assets.pfy.ch/md/000005590020.jpg" alt="" />
<img src="https://assets.pfy.ch/md/000005590024.jpg" alt="" />
<img src="https://assets.pfy.ch/md/000005590025.jpg" alt="" />
<img src="https://assets.pfy.ch/md/000005590029.jpg" alt="" />
<img src="https://assets.pfy.ch/md/000005590036.jpg" alt="" /></p>
<h1 id="kengo-kuma-trek">Kengo Kuma Trek</h1>
<p>Before going to Japan I had picked up a random book at a book store about Kengo Kuma, it had pictures and plans of a lot of their works, and it was all super interesting, so I made it a goal of mine to try check out some buildings that were nearby.</p>
<p>Sadly the Gateway station was covered in Scaffolding, so I couldn&#39;t get a good look at the facade, but I could still appreciate the interesting roof from the inside.</p>
<p>I also walked to Sunny Hills bakery and bought some cakes. I was offered a free cake and tea with my purchase and had a great chat with staff about how I knew about their shop and what I thought of the cakes. They were amazing and I highly recommend them!</p>
<p><img src="https://assets.pfy.ch/md/000005620001.jpg" alt="" />
<img src="https://assets.pfy.ch/md/000005620002.jpg" alt="" />
<img src="https://assets.pfy.ch/md/000005620005.jpg" alt="" />
<img src="https://assets.pfy.ch/md/000005620009.jpg" alt="" />
<img src="https://assets.pfy.ch/md/000005620010.jpg" alt="" /></p>
<h1 id="harajuku">Harajuku</h1>
<p>I didn&#39;t really plan to go to Harajuku or Shibuya on this trip, I don&#39;t speak Japanese, and I was traveling alone, so it didn&#39;t feel like it&#39;d be the best way to experience these areas. However, I did want to visit Meiji Jingu, so I stopped in Harajuku on the way. There was a surprisingly large amount of graffiti in this area which was pretty surprising.</p>
<p><img src="https://assets.pfy.ch/md/000005620011.jpg" alt="" />
<img src="https://assets.pfy.ch/md/000005620012.jpg" alt="" />
<img src="https://assets.pfy.ch/md/000005620013.jpg" alt="" />
<img src="https://assets.pfy.ch/md/000005620014.jpg" alt="" />
<img src="https://assets.pfy.ch/md/000005620015.jpg" alt="" />
<img src="https://assets.pfy.ch/md/000005620016.jpg" alt="" /></p>
<h1 id="meiji-jingu">Meiji Jingu</h1>
<p>I visited Meiji Jingu on one of my last days in Tokyo, it was raining a bit so the area was very quiet. I ended up sitting on a bench in the shrine for a while just enjoying the view. It was very cozy.</p>
<p><img src="https://assets.pfy.ch/md/000005620018.jpg" alt="" />
<img src="https://assets.pfy.ch/md/000005620019.jpg" alt="" />
<img src="https://assets.pfy.ch/md/000005620020.jpg" alt="" />
<img src="https://assets.pfy.ch/md/000005620021.jpg" alt="" />
<img src="https://assets.pfy.ch/md/000005620023.jpg" alt="" /></p>
 ]]></content>
                </entry><entry>
                  <id>https://pfy.ch/games/iidx/iidx-retrospective.html</id>
                  <title>IIDX Retrospective</title>
                  <published>2023-05-03T14:00:00.000Z</published>
                  <updated>2025-01-31T05:59:08.000Z</updated>
                  <link>https://pfy.ch/games/iidx/iidx-retrospective.html</link>
                  <emoji>🎮</emoji>
                  <content type="html" xml:base="https://pfy.ch/games/iidx/iidx-retrospective.html"><![CDATA[ <div class="banner">
<b>Update 27/01/25</b>: It's crazy I wrote this post nearly 2 years ago, I'm still playing IIDX but not as much as I used to. It's funny looking back on this and seeing the naive predictions I made! I should follow this up at some point!
</div>

<div class="banner">
<b>Update 18/05/23:</b> This article was written 05/04/2023, almost
right after I achieved 9th Dan in IIDX 30. I make predictions in
this article about how long I expect it to take to clear 10th Dan. I
cleared it only a month later. There will be a follow-up post
eventually
</div>

<h1 id="iidx-retrospective">IIDX Retrospective</h1>
<p>I&#39;ve been playing IIDX for almost 3 years now and have played
actively through 4 styles<sup><a id="footnote-ref-1" href="#footnote-1" data-footnote-ref aria-describedby="footnote-label">1</a></sup>. Throughout this, I have tracked almost all of my
scores through various means. I wanted to wait until I got Kaiden<sup><a id="footnote-ref-2" href="#footnote-2" data-footnote-ref aria-describedby="footnote-label">2</a></sup>
before I did anything with this data but with Australia just getting
its first Lightning Model<sup><a id="footnote-ref-3" href="#footnote-3" data-footnote-ref aria-describedby="footnote-label">3</a></sup> &amp; Me clearing 9th dan<sup><a id="footnote-ref-4" href="#footnote-4" data-footnote-ref aria-describedby="footnote-label">4</a></sup>
the weekend beforehand I thought it&#39;d be nice to see some stats
about my Pre Lightning Model play history.</p>
<h2 id="all-the-styles">All the styles</h2>
<p>My first style was Heroic Verse, looking back now it wasn&#39;t the best style all
around &amp; the events were kinda mid; But it was my first. I have the AC<sup><a id="footnote-ref-5" href="#footnote-5" data-footnote-ref aria-describedby="footnote-label">5</a></sup>
poster framed up in my flat which I managed to pick up off another local player
here in Sydney.</p>
<div class="graph" key="Chart Plays" style="--max-value: 1721">
<div class="graphInner">
<div class="label"><p>Resident</p></div>
<div class="bar" style="--data: 693"></div>
<div class="event"><p>Resident only released on the 19th of October 2022, this data was taken on the 5th of April 2023.</p></div>
<div class="label"><p>Heroic Verse</p></div>
<div class="bar" style="--data: 726"></div>
<div class="label"><p>Bistrover</p></div>
<div class="bar" style="--data: 1413"></div>
<div class="label"><p>Cast Hour</p></div>
<div class="bar" style="--data: 1721"></div>
</div>
</div>

<p>The overall trend leans towards assuming each style that comes out will lead me to play more. However, Resident is the first style where I&#39;ve been confident enough to play level 11s &amp; 12s<sup><a id="footnote-ref-6" href="#footnote-6" data-footnote-ref aria-describedby="footnote-label">6</a></sup>. If I continue grinding at this rate I could expect to potentially mind-block on certain charts and overall play less, or make the transition to playing BMS<sup><a id="footnote-ref-7" href="#footnote-7" data-footnote-ref aria-describedby="footnote-label">7</a></sup> instead. Regardless, it will be interesting to see future data around this.</p>
<h2 id="plays-between-dan-course-clears">Plays between dan course clears</h2>
<p>I have all my dan course clear dates listed on my website. Using those dates it&#39;s possible to extrapolate plays between clear dates. The data shown on this page is <em>song plays</em> not credits. Total money spent is marked further down...</p>
<div class="graph" key="Chart Plays" style="--max-value: 4553">
<div class="graphInner">
<div class="event"><p>It took a while for me to learn what a Dans were...</p></div>
<div class="label"><p>Before 1st</p></div>
<div class="bar" style="--data: 458"></div>
<div class="label"><p>Before 2nd</p></div>
<div class="bar" style="--data: 483"></div>
<div class="label"><p>Before 3rd</p></div>
<div class="bar" style="--data: 519"></div>
<div class="label"><p>Before 4th</p></div>
<div class="bar" style="--data: 675"></div>
<div class="label"><p>Before 5th</p></div>
<div class="bar" style="--data: 711"></div>
<div class="label"><p>Before 6th</p></div>
<div class="bar" style="--data: 811"></div>
<div class="label"><p>Before 7th</p></div>
<div class="bar" style="--data: 2189"></div>
<div class="label"><p>Before 8th</p></div>
<div class="bar" style="--data: 2617"></div>
<div class="label"><p>Before 9th</p></div>
<div class="bar" style="--data: 4553"></div>
</div>
</div>

<p>As expected this number basically grows logarithmically &amp; I expect it to continue to grow like this.</p>
<p>I am surprised how large the jump is to 7th is as well as the jump to 9th. Or More-so surprised by the lack of massive jumps between the other course clears. 7th Dan has &quot;The Safari&quot; which is notoriously tricky, and 9th Dan has the first 12 in it. I played 9th during Resident so the chart was Todestrieb; Which had chord streams I struggled to follow.</p>
<p>Using the power of &quot;eyeballing&quot; (I don&#39;t know my math too well); We can extrapolate plays to subsequent dan clears. I&#39;m going to be assuming 10th, Chuuden &amp; Kaiden will all be large jumps like the ones for 7th and 9th. With the Jump between 10th &amp; Chuuden being slightly smaller.</p>
<div class="graph" key="Chart Plays" style="--max-value: 20760">
<div class="graphInner">
<div class="label"><p>Before 7th</p></div>
<div class="bar" style="--data: 2189"></div>
<div class="label"><p>Before 8th</p></div>
<div class="bar" style="--data: 2617"></div>
<div class="label"><p>Before 9th</p></div>
<div class="bar" style="--data: 4553"></div>
<div class="label"><p>Before 10th</p></div>
<div class="bar hollow" style="--data: 8650"></div>
<div class="label"><p>Before Chuuden</p></div>
<div class="bar hollow" style="--data: 12975"></div>
<div class="label"><p>Before Kaiden</p></div>
<div class="bar hollow" style="--data: 20760"></div>
</div>
</div>

<p>I assume that 9th to 10th will be a larger jump since it&#39;s the first &quot;All 12s&quot; course. 10th to Chuuden and then to Kaiden will still be exponential jumps but not as large as the jump between 9th and 10th.</p>
<p>Regardless I&#39;m very excited to look back and see how wrong I was!</p>
<h2 id="most-played-charts">Most played charts</h2>
<p>This one is just for fun, there&#39;s not much to extrapolate here other than that I&#39;m a scratchlet.</p>
<div class="graph" key="Chart Plays" style="--max-value: 22">
<div class="graphInner">
<div class="label"><p>Verflucht SPH</p></div>
<div class="bar" style="--data: 10"></div>
<div class="label"><p>Hella Deep SPA</p></div>
<div class="bar" style="--data: 11"></div>
<div class="label"><p>O/D*20 SPA</p></div>
<div class="bar" style="--data: 11"></div>
<div class="label"><p>Shiva SPA</p></div>
<div class="bar" style="--data: 12"></div>
<div class="label"><p>Red. by Jack Trance SPH</p></div>
<div class="bar" style="--data: 12"></div>
<div class="event"><p>I grinded both "Red." charts for Hard Clears.</p></div>
<div class="label"><p>Sigmund SPH</p></div>
<div class="bar" style="--data: 13"></div>
<div class="label"><p>Crank It SPA</p></div>
<div class="bar" style="--data: 14"></div>
<div class="label"><p>KAISER PHOENIX SPH</p></div>
<div class="bar" style="--data: 14"></div>
<div class="label"><p>Red. by Full Metal Jacket SPH</p></div>
<div class="bar" style="--data: 14"></div>
<div class="label"><p>灼熱Beach Side Bunny (Masayoshi Iimori Remix) SPA</p></div>
<div class="bar" style="--data: 14"></div>
<div class="event"><p>This as well as the previous "Beach Side Bunny" charts plays were almost entirely from when I grinded for a hard clear. I did eventually get it on both.</p></div>
<div class="label"><p>Primitive Vibes SPH</p></div>
<div class="bar" style="--data: 15"></div>
<div class="label"><p>Hella Deep SPH</p></div>
<div class="bar" style="--data: 15"></div>
<div class="label"><p>BRAINSTORM SPH</p></div>
<div class="bar" style="--data: 15"></div>
<div class="label"><p>灼熱Beach Side Bunny SPH</p></div>
<div class="bar" style="--data: 19"></div>
<div class="label"><p>SCREW // owo // SCREW SPH</p></div>
<div class="bar" style="--data: 21"></div>
<div class="event"><p>SCREW was the first scratch chart I really got into, I played it heaps over and over again to try and clear it, at the time I could only reliably clear 8's, so it took much longer than expected.</p></div>
<div class="label"><p>THE F∀UST SPH</p></div>
<div class="bar" style="--data: 21"></div>
<div class="label"><p>THE SAFARI SPH</p></div>
<div class="bar" style="--data: 22"></div>
<div class="event"><p>I refused to be a "Safari Refugee" and grinded this chart a bit too much. People really were right when they said: "You'll just get it one day". Because that's exactly what happened.</p>
</div>
</div>
</div>

<h2 id="money-spent">Money spent</h2>
<p>For this section I&#39;m going to make the assumption that a &quot;play&quot; is 3.5 charts. This takes into account Extra Stage as well as the few Premium free credits played in Resident. It&#39;s $2 AUD per play.</p>
<h3 id="by-style">By Style</h3>
<div
  class="graph"
  key="Money Spent Per Style"
  data-prefix="$"
  style="--max-value: 983"
>
<div class="graphInner">
<div class="label"><p>Resident</p></div>
<div class="bar" data-prefix="$" style="--data: 396"></div>
<div class="label"><p>Heroic Verse</p></div>
<div class="bar" data-prefix="$" style="--data: 415"></div>
<div class="label"><p>Bistrover</p></div>
<div class="bar" data-prefix="$" style="--data: 808"></div>
<div class="label"><p>Cast Hour</p></div>
<div class="bar" data-prefix="$" style="--data: 983"></div>
</div>
</div>

<h3 id="by-dan">By Dan</h3>
<p>This data is generated based on plays <em>between</em> clears. Meaning we can extrapolate the most expensive dan clears by seeing how many credits it took between each play to clear it.</p>
<div
  class="graph"
  key="Money spent between dans"
  data-prefix="$"
  style="--max-value: 1106"
>
<div class="graphInner">
<div class="label"><p>Before 2nd</p></div>
<div class="bar" data-prefix="$" style="--data: 14"></div>
<div class="label"><p>Before 3rd</p></div>
<div class="bar" data-prefix="$" style="--data: 21"></div>
<div class="label"><p>Before 4th</p></div>
<div class="bar" data-prefix="$" style="--data: 89"></div>
<div class="label"><p>Before 5th</p></div>
<div class="bar" data-prefix="$" style="--data: 21"></div>
<div class="label"><p>Before 6th</p></div>
<div class="bar" data-prefix="$" style="--data: 57"></div>
<div class="label"><p>Before 7th</p></div>
<div class="bar" data-prefix="$" style="--data: 787"></div>
<div class="label"><p>Before 8th</p></div>
<div class="bar" data-prefix="$" style="--data: 244"></div>
<div class="label"><p>Before 9th</p></div>
<div class="bar" data-prefix="$" style="--data: 1106"></div>
</div>
</div>

<p>As expected 7th &amp; 9th dan are the most expensive <em>so far.</em></p>
<h2 id="where-from-here">Where from here?</h2>
<p>I&#39;m going to continue to track my scores as best as I can, there is very much some data missing from here. Especially if I played 1 song more than once during a credit &amp; then didn&#39;t export my CSV right after. I stayed pretty on top of this but there&#39;s very much some data missing. I&#39;ve played Black by X-Cross Fade <em>way</em> more than my data says I did.</p>
<p>It&#39;ll be extremely interesting to see if my predictions are correct. I&#39;ll definitely create another one of these when I get subsequent dan clears.</p>
<h2 id="thanks">Thanks</h2>
<p>There&#39;s a lot of people I&#39;ve met either directly through IIDX or through rhythm games in general. Without IIDX I would not have met some of my closest friends. I think its cringe to do this sort of thanks at the end of this post, but I very rarely talk about my friends on my website. So without further ado:</p>
<p>To Purikura Sydney for even getting an IIDX cab &amp; to Chris for convincing them to buy it well before I started to play, as well as for being extremely friendly.</p>
<p>To Isaac &amp; Oswald for asking the random white dude watching them play if they&#39;d like a go. Without their friendly attitude and first few pointers I never would have touched the game.</p>
<p>To Martin for being extremely helpful during my first few months playing, teaching me proper tech &amp; for allowing the Australian IIDX community to play on his cab for free during events like Birthdays &amp; BPL, we all very much appreciate the hospitality.</p>
<p>To Amal for helping me get into BMS &amp; configure it correctly and to Lucas as well for helping me and others out with assorted BMS/IIDX tools &amp; clients.</p>
<p>To the entire remaining Australian IIDX community &amp; the English BMS community. They&#39;ve all been extremely helpful &amp; friendly over the last almost four years, and I&#39;m extremely thankful to have gotten to know them.</p>
<p>Most importantly, to Zkldi &amp; Aixee, Without their amazing tools, it would have been impossible to track my plays as thoroughly as I did. Especially to Zkldi since 99% of these scores were tracked via <a class="externalLink" target="_blank" href="https://github.com/tng-dev/tachi">Tachi</a>, which is an amazing tool I&#39;ve watched grow since I started playing.</p>
<p>There are many more people I&#39;ve met through the arcade in general who I&#39;m glad to call my friend, I&#39;m sure they know that I&#39;m grateful for them without me mentioning them by name here.</p>
<section class="footnotes" data-footnotes>
<h2 id="footnote-label" class="sr-only">Footnotes</h2>
<ol>
<li id="footnote-1">
<p>A style is a major version of IIDX, we usually get one every year. They each have a unique theme for the UI &amp; characters. They usually also come with lots of new songs.<sup><a id="footnote-ref-1-2" href="#footnote-1" data-footnote-ref aria-describedby="footnote-label">1</a></sup> <a href="#footnote-ref-1" data-footnote-backref aria-label="Back to reference 1">↩</a> <a href="#footnote-ref-1-2" data-footnote-backref aria-label="Back to reference 1">↩<sup>2</sup></a></p>
</li>
<li id="footnote-2">
<p>Kaiden is the highest level dan course and is considered a &quot;final challenge&quot; in IIDX.<sup><a id="footnote-ref-2-2" href="#footnote-2" data-footnote-ref aria-describedby="footnote-label">2</a></sup> <a href="#footnote-ref-2" data-footnote-backref aria-label="Back to reference 2">↩</a> <a href="#footnote-ref-2-2" data-footnote-backref aria-label="Back to reference 2">↩<sup>2</sup></a></p>
</li>
<li id="footnote-3">
<p>IIDX Arcade cabinets have two models, the latest is &quot;Lightning Model&quot; which debuted with the Heroic Verse style. It features a higher refresh rate screen and updated switches &amp; turntables. It&#39;s overall much nicer to play on.<sup><a id="footnote-ref-3-2" href="#footnote-3" data-footnote-ref aria-describedby="footnote-label">3</a></sup> <a href="#footnote-ref-3" data-footnote-backref aria-label="Back to reference 3">↩</a> <a href="#footnote-ref-3-2" data-footnote-backref aria-label="Back to reference 3">↩<sup>2</sup></a></p>
</li>
<li id="footnote-4">
<p>A dan course is a set of 4 charts you must play in order with a shared health bar. Some argue they&#39;re not the best way to track your skill, but they give you a good idea of how good someone is. Dans go from 1st to 10th, then Chuuden, followed by Kaiden.<sup><a id="footnote-ref-4-2" href="#footnote-4" data-footnote-ref aria-describedby="footnote-label">4</a></sup> <a href="#footnote-ref-4" data-footnote-backref aria-label="Back to reference 4">↩</a> <a href="#footnote-ref-4-2" data-footnote-backref aria-label="Back to reference 4">↩<sup>2</sup></a></p>
</li>
<li id="footnote-5">
<p>AC is referring to &quot;<strong>A</strong>r<strong>c</strong>ade&quot; This is a holdover from when Konami released IIDX styles back on the PlayStation 2. <a href="#footnote-ref-5" data-footnote-backref aria-label="Back to reference 5">↩</a></p>
</li>
<li id="footnote-6">
<p>IIDX charts scale between 1 &amp; 12, with each difficulty (usually) getting exponentially harder than the last. <a href="#footnote-ref-6" data-footnote-backref aria-label="Back to reference 6">↩</a></p>
</li>
<li id="footnote-7">
<p>BMS is a game similar to IIDX that is entirely community driven. It&#39;s sometimes played for practising IIDX, however, a lot of players play only BMS. <a href="#footnote-ref-7" data-footnote-backref aria-label="Back to reference 7">↩</a></p>
</li>
</ol>
</section>
 ]]></content>
                </entry><entry>
                  <id>https://pfy.ch/thoughts/dream-spaces.html</id>
                  <title>Dream spaces</title>
                  <published>2023-04-13T03:50:25.000Z</published>
                  <updated>2024-09-26T06:09:25.000Z</updated>
                  <link>https://pfy.ch/thoughts/dream-spaces.html</link>
                  <emoji>🤔</emoji>
                  <content type="html" xml:base="https://pfy.ch/thoughts/dream-spaces.html"><![CDATA[ <p><img src="https://assets.pfy.ch/md/000042370036.jpg" alt="" /></p>
<h2 id="a-dream-inside-a-dream-but-who-is-the-dreamer">A dream inside a dream, but who is the dreamer?</h2>
<p>I wouldn&#39;t call myself a lucid dreamer, in fact I&#39;m quite the opposite. My memory of late has been terrible, and I can hardly remember anything I dream about unless prompted. However, dream spaces, or at least mine; are constructed of things that exist within my reality. Perceiving these things can trigger déjà vu back to these spaces I&#39;ve dreamt about in the past.</p>
<h2 id="schrdingers-space">Schrödinger's space</h2>
<p>These spaces can be split into two categories, <em>subconscious spaces</em> and <em>conscious spaces</em>. The most interesting ones are the spaces created by the subconscious. Spaces that, while present in them dreaming, are recognizable &amp; recurring. But when awake, are instantly forgettable. With your conscious recalling them when primed by something that existed within them; A person, object, or item. </p>
<h2 id="once-perceived-its-no-longer-a-dream-space">*Once perceived it's no longer a dream space.*</h2>
<p>Once a subconscious space is perceived enough when awake, I&#39;d consider the space to be different. It&#39;s now a space constructed in the conscious mind; No longer an amalgamation of separate thoughts &amp; memories, but instead its own significant constructed place to be thrown into memory banks to be churned out into something new in a dream.</p>
<p>This space is no longer a sum of its parts, but a part itself. Something that future dream spaces can pick from, creating hybrids of places that seem similar but may not be.</p>
<pre><code class="hljs language-mermaid">flowchart LR
    Experiences --&gt; PlaceA[&quot;Place A&quot;]
    Experiences --&gt; PlaceB[&quot;Place B&quot;]
    Experiences --&gt; PlaceC[&quot;Place C&quot;]
    
    subgraph Memory
        PlaceA
        PlaceB
        PlaceC
        NewSpace[New Space]
    end

    subgraph Subconscious
        PlaceA --&gt; DreamState[Dream Space]
        PlaceB --&gt; DreamState
        PlaceC --&gt; DreamState
        NewSpace[New Space] --&gt; DreamState
    end

    subgraph Conscious
        DreamState -- Recall the dream space --&gt; PerceivingMind[Perceiving Mind]
        PerceivingMind -- Recalling becomes memory --&gt; NewSpace
    end
</code></pre><p><img src="https://assets.pfy.ch/md/000042370025.jpg" alt="" /></p>
<p>Once remembered, the place no longer exists as you perceived it.</p>
 ]]></content>
                </entry><entry>
                  <id>https://pfy.ch/about.html</id>
                  <title>Pfych</title>
                  <published>2023-03-23T03:50:25.000Z</published>
                  <updated>2025-05-24T01:05:50.000Z</updated>
                  <link>https://pfy.ch/about.html</link>
                  <emoji>📝</emoji>
                  <content type="html" xml:base="https://pfy.ch/about.html"><![CDATA[ <p><img src="https://assets.pfy.ch/md/DSCF0594.jpg" alt="" /></p>
<p>This page is a little barren ...</p>
<p>I&#39;m Pfych (she/her), a full time developer &amp; hobbyist something or other in the Sydney region.</p>
<p>This site is full of personal projects, thoughts, photos and ideas that I&#39;ve been working on for the past few years. I&#39;m always looking for new things to do, so if you&#39;re interested in something, feel free to reach out!</p>
<ul>
<li><a class=""  href="experience.html">Professional Experience</a> (A formal &amp; boring)</li>
<li><a class=""  href="games/games.html">Games I'm into</a></li>
<li><a class=""  href="music/music.html">Music I'm into</a></li>
<li><a class=""  href="programming/programming.html">Recent projects I've worked on</a><ul>
<li><a class="externalLink" target="_blank" href="https://github.com/pfych">GitHub</a></li>
<li><a class="externalLink" target="_blank" href="https://git.pfy.ch">GitTea</a> (Self-hosted, mostly private)</li>
</ul>
</li>
<li><a class=""  href="/images.html">Photo's I've taken</a></li>
</ul>
 ]]></content>
                </entry><entry>
                  <id>https://pfy.ch/programming/hosting-static-on-aws.html</id>
                  <title>$0 Static site hosting with Amazon</title>
                  <published>2022-12-19T03:50:25.000Z</published>
                  <updated>2024-12-01T21:07:17.000Z</updated>
                  <link>https://pfy.ch/programming/hosting-static-on-aws.html</link>
                  <emoji>💻</emoji>
                  <content type="html" xml:base="https://pfy.ch/programming/hosting-static-on-aws.html"><![CDATA[ <p>Nothing beats hosting your website on your own personal server, however, this is not an option for a lot of people due to either complexity or cost.</p>
<p>Part of my current job involves me working with Amazon Web Services infrastructure (basically fancy servers). By taking advantage of Amazons free tiers, it is possible to host your own static webpage entirely for free with very little programming know-how!</p>
<p>This guide will assume you know nothing about Amazon or about HTML. We&#39;ll start by building a simple HTML page, and then we&#39;ll deploy it to Amazon manually. This is all you really need! However, afterwards, I&#39;ll walk you through setting up Budgets in AWS Cost Management to avoid <em>any</em> bills over a dollar (just in case!).</p>
<h1 id="how-is-it-free">How is it free?</h1>
<p>Amazon Web Services (AWS) is behind an extremely large chunk of the internet as a whole and is one of Amazons main methods of revenue production, this allows Amazon to benefit from economies of scale. Basically, they have so many large clients that they are able to offer extremely generous free tiers to smaller users in the hopes that eventually they&#39;ll scale up and start paying, or they&#39;ll recommend AWS to their employer or company.</p>
<p>Since we&#39;re just making a personal website, this generous free tier will let us host whatever we want for very little!</p>
<p>Let&#39;s break down the potential costs really quick:</p>
<p>For storage, you get the following for free every month:</p>
<ul>
<li>5 GB of data</li>
<li>20,000 requests</li>
<li>2,000 uploads</li>
<li>100 GB transfer limit</li>
</ul>
<p>After going over 5 GB, you&#39;re charged $0.023 per GB over. For a simple personal site, you&#39;ll never go over this storage limit.</p>
<p>20,000 requests refers to any files your site uses, if you just have a single HTML page that means 20,000 people can view your page monthly. However, once you exceed 20,000 requests you&#39;re charged $0.0004 per 1000 extra requests (yes, that&#39;s the right amount of zeros!).</p>
<p>We can add caching to lower the amount of requests made, caching basically saves frequently accessed files by users to avoid people &quot;double getting&quot; a file. This makes the site feel faster to them and saves us money!</p>
<p>Amazons Caching service is free up to 1 TB and 10,000,000 requests. Once you exceed 10,000,000 requests, you pay $0.0075 per 10,000 requests after.</p>
<p>Later in this guide, we&#39;ll set up a Budget on Amazon to alert us via email if any of these values  are about to go out of the free tier.</p>
<h1 id="making-a-basic-html-site">Making a basic HTML site</h1>
<p>This won&#39;t be a guide on how HTML works, or a guide walking you through designing your own website. HTML can be super fun, and I really recommend using <a class="externalLink" target="_blank" href="https://developer.mozilla.org/en-US/docs/Learn/Getting_started_with_the_web/HTML_basics">Mozilla's MDN</a> for explanations on what everything does. Or if you&#39;re a complete beginner, check out <a class="externalLink" target="_blank" href="https://htmlforpeople.com/">HTML for People</a> which is a great resource for non-technical people.</p>
<p>Using any text editor, preferably something like <a class="externalLink" target="_blank" href="https://code.visualstudio.com/">vscode</a> if you&#39;re a beginner. Create a file called <code>index.html</code> inside its own folder on your computer somewhere safe.</p>
<p>Inside this file, paste the following:</p>
<pre><code class="hljs language-html"><span class="hljs-tag">&lt;<span class="hljs-name">html</span> <span class="hljs-attr">lang</span>=<span class="hljs-string">&quot;en-US&quot;</span>&gt;</span>
	<span class="hljs-tag">&lt;<span class="hljs-name">head</span>&gt;</span>
	    <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">charset</span>=<span class="hljs-string">&quot;utf-8&quot;</span> /&gt;</span>
	    <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">name</span>=<span class="hljs-string">&quot;viewport&quot;</span> <span class="hljs-attr">content</span>=<span class="hljs-string">&quot;width=device-width&quot;</span> /&gt;</span>
	    <span class="hljs-tag">&lt;<span class="hljs-name">title</span>&gt;</span>My Personal Page!<span class="hljs-tag">&lt;/<span class="hljs-name">title</span>&gt;</span>
	<span class="hljs-tag">&lt;/<span class="hljs-name">head</span>&gt;</span>
	<span class="hljs-tag">&lt;<span class="hljs-name">body</span>&gt;</span>
		<span class="hljs-tag">&lt;<span class="hljs-name">h1</span>&gt;</span>This is a heading!<span class="hljs-tag">&lt;/<span class="hljs-name">h1</span>&gt;</span>
		<span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>This is a paragraph<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
	<span class="hljs-tag">&lt;/<span class="hljs-name">body</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">html</span>&gt;</span>
</code></pre><p>There&#39;s some super basic stuff in here, and hopefully you can sort of figure out just by looking at what all of these do (don&#39;t worry about the <code>meta</code> stuff for now!)</p>
<p>If you save this file, and then open your File explorer and double-click on it, it should open a super simple webpage in your browser of choice!</p>
<p>If you want to learn more about HTML, check out <a class="externalLink" target="_blank" href="https://developer.mozilla.org/en-US/docs/Learn/Getting_started_with_the_web/HTML_basics">Mozilla's beginners guide</a>, once you&#39;re comfortable with HTML, you can start making it look pretty and custom by using CSS, <a class="externalLink" target="_blank" href="https://developer.mozilla.org/en-US/docs/Learn/Getting_started_with_the_web/CSS_basics">Mozilla also has a beginner guide for this available</a>!</p>
<h1 id="creating-an-aws-account">Creating an AWS account</h1>
<p>Creating an AWS account is super quick!</p>
<ol>
<li>Navigate to the <a class="externalLink" target="_blank" href="https://aws.amazon.com/">AWS homepage</a></li>
<li>Click &quot;Create an AWS Account&quot; in the top right of the page</li>
<li>Enter your email address and pick an account name</li>
<li>Click verify email address and enter the code sent through to you</li>
<li>Pick a secure password! I <em><strong>highly</strong></em> recommend making this 16+ characters long with numbers and special characters. Make sure to write this down somewhere!</li>
<li>Once this is done, select &quot;Personal account&quot; and fill out your details</li>
<li>Enter your credit card details</li>
<li>Enter your mobile number again to receive a text confirmation code</li>
<li>Select the Basic Support free tier</li>
</ol>
<p>You should now be re-prompted to log in.</p>
<ol>
<li>Select &quot;Root user&quot; and enter your email address.</li>
<li>You&#39;ll then be prompted to enter your password.</li>
</ol>
<p>Once this is done, you&#39;ll now be on the AWS Console. In the top right to the left of your name you should see a list of regions. <strong>Select the region closest to you.</strong></p>
<h1 id="deploying-your-website-manually">Deploying your website manually</h1>
<p>You could use a toolkit like Serverless to automatically build a CloudFormation and manage your website, however that is out of scope for this guide. We&#39;ll be deploying manually for now.</p>
<h2 id="deploying-to-s3">Deploying to S3</h2>
<p>Navigate to S3 in the Amazon dashboard and click &quot;Create Bucket&quot;. You can name the bucket whatever you want, but it must unique globally (meaning nobody else is using this name). <strong>Make sure the region is the one closest to you.</strong></p>
<ol>
<li>Uncheck &quot;Block all public access&quot; and then tick that you acknowledge the risks.</li>
<li>Leave everything as is and click &quot;Create bucket&quot;</li>
</ol>
<p>Next, navigate to your bucket by clicking its name.</p>
<ol>
<li>Click on Permissions</li>
<li>Scroll down to Bucket Policy</li>
<li>Click edit</li>
<li>Paste the following (<strong>and replace &quot;<code>your-bucket-name-here</code>&quot; with your buckets name</strong>):</li>
</ol>
<pre><code class="hljs language-json"><span class="hljs-punctuation">{</span>
    <span class="hljs-attr">&quot;Version&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;2008-10-17&quot;</span><span class="hljs-punctuation">,</span>
    <span class="hljs-attr">&quot;Statement&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span>
        <span class="hljs-punctuation">{</span>
            <span class="hljs-attr">&quot;Sid&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;PublicReadGetObject&quot;</span><span class="hljs-punctuation">,</span>
            <span class="hljs-attr">&quot;Effect&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;Allow&quot;</span><span class="hljs-punctuation">,</span>
            <span class="hljs-attr">&quot;Principal&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;*&quot;</span><span class="hljs-punctuation">,</span>
            <span class="hljs-attr">&quot;Action&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;s3:GetObject&quot;</span><span class="hljs-punctuation">,</span>
            <span class="hljs-attr">&quot;Resource&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;arn:aws:s3:::your-bucket-name-here/*&quot;</span>
        <span class="hljs-punctuation">}</span>
    <span class="hljs-punctuation">]</span>
<span class="hljs-punctuation">}</span>
</code></pre><p>Once this is saved:</p>
<ol>
<li>Navigate to &quot;Objects&quot;</li>
<li>Click upload.</li>
<li>Click add files</li>
<li>Select all your site&#39;s content.</li>
<li>Scroll down and click Upload.</li>
</ol>
<p>Once the upload is complete, close the upload popup by clicking close in the top right.</p>
<ol>
<li>Navigate to &quot;Properties&quot;</li>
<li>Scroll to the bottom.</li>
<li>Click &quot;Edit&quot; on static web hosting.</li>
<li>Enable static web hosting.</li>
<li>Inside the index document input type: <code>index.html</code><ol>
<li>Add an error document if you&#39;ve made one, this will be your 404 page!</li>
</ol>
</li>
<li>Click save changes.</li>
</ol>
<p>If you scroll down to the bottom again, you should now see a URL, this is your site however it is not caching, We&#39;ll set that up now.</p>
<h2 id="setting-up-cloudfront">Setting up CloudFront</h2>
<p>From the Amazon dashboard home, navigate to the service called CloudFront. You may have to search for it using the top search bar.</p>
<ol>
<li>Click &quot;Create CloudFront distribution&quot;</li>
<li>Select your S3 bucket from the origin domain dropdown.</li>
<li>Scroll down to settings</li>
<li>Set &quot;Default root object&quot; to <code>index.html</code></li>
<li>Click &quot;Create distribution&quot;</li>
</ol>
<p>This will take a few minutes to deploy. Once deployed, you can navigate to the CloudFront domain displayed in the distribution to view your site! If you make any changes to the site and want to clear the CloudFront cache, you can optionally do the following:</p>
<ol>
<li>Navigate to &quot;Invalidation&quot;</li>
<li>Create new Invalidation</li>
<li>Type <code>/*</code></li>
<li>Click create</li>
</ol>
<p>CloudFront will automatically invalidate eventually, so this is optional.</p>
<h1 id="setting-up-an-amazon-budget">Setting up an Amazon budget</h1>
<p>To avoid any surprise bills, you can set up an Amazon Budget. Go back to the Amazon Dashboard and Navigate to &quot;AWS Cost Explorer&quot;, you may need to search for this.</p>
<ol>
<li>In the left sidebar, near the top, click Budgets.</li>
<li>Click &quot;Create a budget&quot;</li>
<li>Select &quot;Use a Template&quot; and then &quot;Monthly Cost Budget&quot;</li>
<li>Give this budget a name</li>
<li>Set the max you ever want to spend (I set this to a dollar).</li>
<li>Under email recipients, put any emails you want to be alerted if you&#39;re about to hit this budget.</li>
<li>Click &quot;Create Budget&quot;.</li>
</ol>
<p>You&#39;ll now receive an email at 85% of the budget and a 100% of the budget.</p>
<h1 id="domains">Domains</h1>
<p>Amazon offers a service called Route 53 which can manage your DNS as well as buy a new or manage an existing domain directly. This service costs $0.50 a month per domain, but makes connecting URLs to Amazon services and managing SSL certificates crazy easy. I will not be going in depth on this since there are plenty of guides online.</p>
<h1 id="in-closing">In closing</h1>
<p>You&#39;re all set up with a static webpage! Do note that this site is <em>static</em>, meaning that all the code and content is pre-written. You cannot use a service like WordPress or Ghost to manage blog posts or pages. If you wish to stay free, I recommend avoiding services like EC2 &amp; Amplify, as they can quickly get out of hand if you&#39;re unsure what you&#39;re doing.</p>
<p>I use a command line tool called <a class="externalLink" target="_blank" href="https://pandoc.org/">Pandoc</a> to convert my posts into HTML, which I write and store as Markdown in <a class="externalLink" target="_blank" href="https://obsidian.md/">Obsidian</a><sup><a id="footnote-ref-1" href="#footnote-1" data-footnote-ref aria-describedby="footnote-label">1</a></sup>. I&#39;ll eventually release the scripts I use to automatically convert my Markdown notes to this site.</p>
<p>Eventually I plan on making a guide soon for how to host an API for free using AWS Lambda, I&#39;m aware that using a service like <a class="externalLink" target="_blank" href="https://pages.github.com/">GitHub Pages</a> will also allow you to host a static site for free but by keeping everything under AWS it makes connecting domains, SSL and eventually linking up an API all extremely easy (and extremely cheap).</p>
<p>If you have any questions, please feel free to email me. However, please do some research on your own before reaching out! Mozilla&#39;s MDN is a great resource, and so is Stack Overflow if you have any generic questions.</p>
<section class="footnotes" data-footnotes>
<h2 id="footnote-label" class="sr-only">Footnotes</h2>
<ol>
<li id="footnote-1">
<p><strong>Update 2024/12/02:</strong> I still use markdown but it&#39;s gotten a bit more complex, obsidian can work great but I&#39;ve moved to using other tools. I&#39;ll eventually write about this. <a href="#footnote-ref-1" data-footnote-backref aria-label="Back to reference 1">↩</a></p>
</li>
</ol>
</section>
 ]]></content>
                </entry><entry>
                  <id>https://pfy.ch/programming/programming.html</id>
                  <title>Programming</title>
                  <published>2022-11-14T00:00:00.000Z</published>
                  <updated>2025-07-05T06:00:46.000Z</updated>
                  <link>https://pfy.ch/programming/programming.html</link>
                  <emoji>💻</emoji>
                  <content type="html" xml:base="https://pfy.ch/programming/programming.html"><![CDATA[ <p>I do a little coding - I don&#39;t really have any large personal projects I can share, but I do have some assorted guides and small tinkering projects hosted here.</p>
<h2 id="guides-articles">Guides & Articles</h2>
<ul>
<li><a class=""  href="./nix/good-actually.html">Nix is good actually?</a> <updated-at date="1774521169299"></updated-at></li>
<li><a class=""  href="./splitting-mcp-saves.html">Splitting .mc2 memory cards</a> <updated-at date="1761959607067"></updated-at></li>
<li><a class=""  href="setting-up-printer-arch.html">Setting up a Brother HL-L2400DW on Arch</a></li>
<li><a class=""  href="gamemoderun.html">Setting up gamemoderun correctly</a> <updated-at date="1750484675000"></updated-at></li>
<li><a class=""  href="the-images-page.html">The Images Page</a></li>
<li><a class=""  href="webmentions.html">Re-enabling Webmentions</a></li>
<li><a class=""  href="bluesky-comments.html">BlueSky Comments</a></li>
<li><a class=""  href="./jsx-clientside.html">JSX Clientside - using JSX on a static site</a></li>
<li><a class=""  href="cloudfront-reverse-proxy.html">Using Cloudfront as a reverse proxy</a></li>
<li><a class=""  href="homebridge-via-moonraker.html">Control a HomeBridge Accessory with Moonraker</a></li>
<li><a class=""  href="4k-sunshine.html">4K Desktop Streaming from a 1080p Wayland Host</a></li>
<li><a class=""  href="pipewire-steam-remote-play.html">PipeWire & Steam Remote Play</a></li>
<li><a class=""  href="packaging-for-arch.html">Packaging for Arch Linux</a></li>
<li><a class=""  href="opl-mode-one.html">OPL, SMB & Mode 1</a></li>
<li><a class=""  href="func-godot.html">Setting up func_godot</a></li>
<li><a class=""  href="languages/typescript/create-a-google-sheet-from-a-template.html">Create a Google sheet from a template</a></li>
<li><a class=""  href="languages/typescript/facebook-oauth-cognito.html">Facebook Oauth in Cognito without Amplify CLI or UI</a></li>
<li><a class=""  href="hosting-static-on-aws.html">Hosting a static site on AWS</a></li>
<li><a class=""  href="bash.html">Bash talk notes</a></li>
<li><a class=""  href="serverless-please.html">Serverless Please</a></li>
<li><a class=""  href="study/SAA-C03.html">SAA-C03 Digitised notes</a></li>
<li><a class=""  href="study/SAP-C02.html">SAP-C02 Digitised notes</a></li>
<li><a class=""  href="../experience.html">Professional Experience & Work History</a></li>
</ul>
<h2 id="mini-projects">Mini Projects</h2>
<ul>
<li><a class=""  href="projects/mangadex-chapters.html">Mangadex Chapter Checker</a> - Check if new chapters have released for tracked Manga </li>
<li><a class=""  href="../music/scrobbler.html">Rockbox Scrobbler</a> - Rockbox playback log to Lastfm</li>
<li><a class=""  href="../games/mahjong/scoring-calculator.html">Riichi Mahjong Scoring Calculator</a> - Simple scoring calculator for Riichi Mahjong</li>
<li><a class=""  href="experiments/README.html">Experiments</a> - Experiments done on my commute home from work</li>
<li><a class=""  href="projects/gitadora.html">Gitadora to Kamaitachi</a> - Importing your Gitadora scores to Kamaitachi</li>
<li><a class=""  href="projects/sandbox.html">Sandbox</a> - Web based programming environment</li>
<li><a class=""  href="projects/render-table-from-data.html">Render Tables</a> - Render tables from JS Data</li>
<li><a class=""  href="projects/webmentions.html">Webmention Receiver</a> - Serverless Webmention receiver</li>
<li><a class=""  href="projects/bms-sow.html">BMS Scope of Work</a> - SOW for a BMS related project</li>
<li><a class=""  href="/iidx-guide.html">IIDX Beginners Guide</a> - Beginners guide for IIDX, animated with CSS</li>
<li><a class=""  href="../games/iidx/iidx-retrospective.html">IIDX Retrospective</a> - Retrospective of game progress, charts done with CSS</li>
</ul>
 ]]></content>
                </entry><entry>
                  <id>https://pfy.ch/photography/concerts/ramirez.html</id>
                  <title>Ramirez</title>
                  <published>2022-09-21T15:00:00.000Z</published>
                  
                  <link>https://pfy.ch/photography/concerts/ramirez.html</link>
                  <emoji>🎤</emoji>
                  <content type="html" xml:base="https://pfy.ch/photography/concerts/ramirez.html"><![CDATA[ <p>Massive shout-out to WavyLand for inviting me out to shoot this concert. All photos on the night were shot on my Fuji DL-100 with Kodak Portra 800 film. The crowd was wild and the vibes were good. Was chill meeting everyone on their team as well as meeting Ramirez. Was dope chatting backstage about music.</p>
<h2 id="the-show">The Show</h2>
<p>I now have a mad respect for show photographers, snapping pics of artists while they do their thing is hard af.</p>
<p><img src="https://assets.pfy.ch/md/CRO8810-R1-01-35.jpg" alt="" />
<img src="https://assets.pfy.ch/md/CRO8810-R1-07-29.jpg" alt="" />
<img src="https://assets.pfy.ch/md/CRO8810-R1-10-26.jpg" alt="" />
<img src="https://assets.pfy.ch/md/CRO8810-R1-12-24.jpg" alt="" />
<img src="https://assets.pfy.ch/md/CRO8810-R1-15-21.jpg" alt="" />
<img src="https://assets.pfy.ch/md/CRO8810-R1-16-20.jpg" alt="" />
<img src="/images/CRO8810-R1-27-9.jpg" alt="Thrown beer" />
<img src="https://assets.pfy.ch/md/CRO8810-R1-19-17.jpg" alt="" />
<img src="https://assets.pfy.ch/md/CRO8810-R1-20-16.jpg" alt="" />
<img src="https://assets.pfy.ch/md/CRO8810-R1-23-13.jpg" alt="" />
<img src="https://assets.pfy.ch/md/CRO8810-R1-33-3.jpg" alt="" />
<img src="https://assets.pfy.ch/md/CRO8811-R1-05-7A.jpg" alt="" />
<img src="https://assets.pfy.ch/md/CRO8811-R1-20-22A.jpg" alt="" />
<img src="https://assets.pfy.ch/md/CRO8811-R1-26-28A.jpg" alt="" /></p>
<h2 id="train-graf">Train Graf</h2>
<p>Before the show I had about 15 exposures left on a roll of UltraMax &amp; I had zero idea how to get through it. While waiting for my train, freight came through. Did my best to shoot while it rolled through. </p>
<p><img src="https://assets.pfy.ch/md/CRO8812-R1-14-10.jpg" alt="" />
<img src="https://assets.pfy.ch/md/CRO8812-R1-15-9.jpg" alt="" />
<img src="https://assets.pfy.ch/md/CRO8812-R1-16-8.jpg" alt="" />
<img src="https://assets.pfy.ch/md/CRO8812-R1-17-7.jpg" alt="" />
<img src="https://assets.pfy.ch/md/CRO8812-R1-18-6.jpg" alt="" />
<img src="https://assets.pfy.ch/md/CRO8812-R1-19-5.jpg" alt="" />
<img src="https://assets.pfy.ch/md/CRO8812-R1-20-4.jpg" alt="" />
<img src="https://assets.pfy.ch/md/CRO8812-R1-21-3.jpg" alt="" />
<img src="https://assets.pfy.ch/md/CRO8812-R1-22-2.jpg" alt="" /></p>
 ]]></content>
                </entry><entry>
                  <id>https://pfy.ch/photography/concerts/machine-girl.html</id>
                  <title>Machine Girl</title>
                  <published>2022-06-21T15:00:00.000Z</published>
                  <updated>2025-01-07T05:31:27.000Z</updated>
                  <link>https://pfy.ch/photography/concerts/machine-girl.html</link>
                  <emoji>🎤</emoji>
                  <content type="html" xml:base="https://pfy.ch/photography/concerts/machine-girl.html"><![CDATA[ <p>On the 22nd of June I went and saw Machine Girl live with some mates, The show was insane and the energy was unmatched by almost any show I&#39;ve ever been to. Honestly was one of my favourite live shows to date.</p>
<h1 id="the-photos">The Photos</h1>
<p>Almost all of these came out good, I&#39;ll be including them in chronological order with only (non-cool) blurry messes removed. None of these images have been edited.</p>
<h2 id="ultramax-400-iso">Ultramax (400 ISO)</h2>
<p>I had a few shots left in the camera during the opening DJ set.</p>
<p><img src="https://assets.pfy.ch/md/CRO3745-R1-12-12A.jpg" alt="" />
<img src="https://assets.pfy.ch/md/CRO3745-R1-13-11A.jpg" alt="" />
<img src="https://assets.pfy.ch/md/CRO3745-R1-14-10A.jpg" alt="" />
<img src="https://assets.pfy.ch/md/CRO3745-R1-17-7A.jpg" alt="" />
<img src="https://assets.pfy.ch/md/CRO3745-R1-21-3A.jpg" alt="" /></p>
<h2 id="porta-800-iso">Porta (800 ISO)</h2>
<p>Finally whipped out this film, I was super worried they&#39;d all come out shit since I&#39;d never shot with it before but I&#39;m insanely happy with the outcome.</p>
<p><img src="https://assets.pfy.ch/md/CRO6452-R1-00-36A.jpg" alt="" />
<img src="https://assets.pfy.ch/md/CRO6452-R1-01-35A.jpg" alt="" />
<img src="https://assets.pfy.ch/md/CRO6452-R1-02-34A.jpg" alt="" />
<img src="https://assets.pfy.ch/md/CRO6452-R1-03-33A.jpg" alt="" />
<img src="https://assets.pfy.ch/md/CRO6452-R1-04-32A.jpg" alt="" />
<img src="https://assets.pfy.ch/md/CRO6452-R1-05-31A.jpg" alt="" />
<img src="https://assets.pfy.ch/md/CRO6452-R1-06-30A.jpg" alt="" />
<img src="https://assets.pfy.ch/md/CRO6452-R1-07-29A.jpg" alt="" />
<img src="https://assets.pfy.ch/md/CRO6452-R1-08-28A.jpg" alt="" />
<img src="https://assets.pfy.ch/md/CRO6452-R1-09-27A.jpg" alt="" />
<img src="https://assets.pfy.ch/md/CRO6452-R1-10-26A.jpg" alt="" />
<img src="https://assets.pfy.ch/md/CRO6452-R1-11-25A.jpg" alt="" />
<img src="https://assets.pfy.ch/md/CRO6452-R1-12-24A.jpg" alt="" />
<img src="https://assets.pfy.ch/md/CRO6452-R1-13-23A.jpg" alt="" />
<img src="https://assets.pfy.ch/md/CRO6452-R1-14-22A.jpg" alt="" />
<img src="https://assets.pfy.ch/md/CRO6452-R1-15-21A.jpg" alt="" />
<img src="https://assets.pfy.ch/md/CRO6452-R1-16-20A.jpg" alt="" />
<img src="https://assets.pfy.ch/md/CRO6452-R1-17-19A.jpg" alt="" />
<img src="https://assets.pfy.ch/md/CRO6452-R1-18-18A.jpg" alt="" />
<img src="https://assets.pfy.ch/md/CRO6452-R1-19-17A.jpg" alt="" />
<img src="https://assets.pfy.ch/md/CRO6452-R1-20-16A.jpg" alt="" />
<img src="https://assets.pfy.ch/md/CRO6452-R1-21-15A.jpg" alt="" />
<img src="https://assets.pfy.ch/md/CRO6452-R1-22-14A.jpg" alt="" />
<img src="https://assets.pfy.ch/md/CRO6452-R1-23-13A.jpg" alt="" />
<img src="https://assets.pfy.ch/md/CRO6452-R1-24-12A.jpg" alt="" />
<img src="https://assets.pfy.ch/md/CRO6452-R1-25-11A.jpg" alt="" />
<img src="https://assets.pfy.ch/md/CRO6452-R1-26-10A.jpg" alt="" />
<img src="https://assets.pfy.ch/md/CRO6452-R1-27-9A.jpg" alt="" />
<img src="https://assets.pfy.ch/md/CRO6452-R1-28-8A.jpg" alt="" /></p>
<h1 id="thanks-updates">Thanks / Updates</h1>
<p>Shout-outs to the group I was with before and after the show, your company (even if brief) was mad.</p>
<p><img src="https://assets.pfy.ch/md/CRO3745-R1-09-15A-censor.jpg" alt="" /></p>
 ]]></content>
                </entry>
  </feed>
  