<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>notnite&apos;s blog</title><description>My ramblings assembled into one tidy place</description><link>https://notnite.com/</link><item><title>Running Vintage Story in Docker, the lazy way</title><link>https://notnite.com/blog/vintage-story-docker</link><guid isPermaLink="true">https://notnite.com/blog/vintage-story-docker</guid><description>Powered by the joys of the .NET Runtime</description><pubDate>Thu, 16 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;This post is a short one, I just thought it was fun and/or useful to write about. My friend group is starting their seasonal &lt;a href=&quot;https://www.vintagestory.at/&quot;&gt;Vintage Story&lt;/a&gt; addiction, which means I’m in charge of setting up a dedicated server.&lt;/p&gt;
&lt;p&gt;The Vintage Story dedicated server comes with its own &lt;code&gt;server.sh&lt;/code&gt; shell script (which seems to be &lt;a href=&quot;https://wiki.vintagestory.at/Guide:Dedicated_Server#Dedicated_server_on_Linux&quot;&gt;the official setup method&lt;/a&gt;). It runs the game under a screen session, which I’m not a fan of, as I’d prefer to use my existing Docker setup. It also requires you to install the .NET Runtime, which I didn’t want to do (and &lt;em&gt;couldn’t&lt;/em&gt; do back when Vintage Story still used .NET 7).&lt;/p&gt;
&lt;p&gt;However, because Vintage Story is written in C# and uses the modern .NET Runtime, you can just skip &lt;code&gt;server.sh&lt;/code&gt; entirely and execute &lt;code&gt;VintagestoryServer.dll&lt;/code&gt; using the dotnet CLI. This also makes my life easier, since I don’t need a custom Dockerfile and I can just use the &lt;a href=&quot;https://github.com/dotnet/dotnet-docker&quot;&gt;official .NET images&lt;/a&gt;. I’m used to updating games with &lt;a href=&quot;https://developer.valvesoftware.com/wiki/SteamCMD&quot;&gt;SteamCMD&lt;/a&gt;, so I prefer mounting the game install as a volume instead of baking it into the image.&lt;/p&gt;
&lt;p&gt;The dedicated server can be easily downloaded without authentication, so I just unzip it into a folder:&lt;/p&gt;
&lt;pre class=&quot;language-shell&quot; data-language=&quot;shell&quot;&gt;&lt;code class=&quot;language-shell&quot;&gt;&lt;span class=&quot;token function&quot;&gt;wget&lt;/span&gt; https://cdn.vintagestory.at/gamefiles/stable/vs_server_linux-x64_1.21.5.tar.gz
&lt;span class=&quot;token function&quot;&gt;mkdir&lt;/span&gt; ./server
&lt;span class=&quot;token function&quot;&gt;tar&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-xf&lt;/span&gt; ./vs_server_linux-x64_1.21.5.tar.gz &lt;span class=&quot;token parameter variable&quot;&gt;-C&lt;/span&gt; ./server
&lt;span class=&quot;token function&quot;&gt;rm&lt;/span&gt; ./vs_server_linux-x64_1.21.5.tar.gz
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Finally, I just created a &lt;code&gt;compose.yaml&lt;/code&gt; with the official .NET Runtime image:&lt;/p&gt;
&lt;pre class=&quot;language-yaml&quot; data-language=&quot;yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;services&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;vintagestory&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; mcr.microsoft.com/dotnet/runtime&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;8.0&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;restart&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; unless&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;stopped
    &lt;span class=&quot;token key atrule&quot;&gt;ports&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;42420:42420/tcp&quot;&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;volumes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; ./server&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;/var/vintagestory/server&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;ro
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; ./data&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;/var/vintagestory/data
    &lt;span class=&quot;token key atrule&quot;&gt;command&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;dotnet&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;/var/vintagestory/server/VintagestoryServer.dll&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;--dataPath&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;/var/vintagestory/data&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After starting the server, it’s easy to edit things in the &lt;code&gt;data&lt;/code&gt; directory (e.g. put mods into the &lt;code&gt;Mods&lt;/code&gt; folder or edit &lt;code&gt;serverconfig.json&lt;/code&gt;). Problem solved!&lt;/p&gt;</content:encoded></item><item><title>Upgrading my home network to UniFi</title><link>https://notnite.com/blog/unifi</link><guid isPermaLink="true">https://notnite.com/blog/unifi</guid><description>Now I can post with even faster speeds</description><pubDate>Tue, 22 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;One morning, after waking up and dragging myself to my desk, I logged into my computer only to find out that I couldn’t connect to the Internet. Annoyed, I opened my router’s interface in my browser to go diagnose the issue. I began to ask myself if my ISP had broken something upstream again, or maybe if I had accidentally uninstalled my network drivers again. However, the connection to my router &lt;em&gt;timed out&lt;/em&gt;!&lt;/p&gt;
&lt;p&gt;At this point, I began to realize this would be a far more annoying problem to deal with today. This meant that there was either A) total failure of my downstairs networking equipment or B) the WiFi access point had failed. To rule this out, I went to my website from my phone, which was hosted on my home server downstairs. Unfortunately, it loaded fine…&lt;/p&gt;
&lt;p&gt;That meant it was the fault of my WiFi! For context, I use a mesh WiFi network, because running network cables through my house would be a ton of work. Specifically, I use an older revision of the &lt;a href=&quot;https://www.netgear.com/home/wifi/mesh/&quot;&gt;NETGEAR Orbi&lt;/a&gt;. There’s an access point downstairs where the router is, and one upstairs at my desk that I connect to via Ethernet.&lt;/p&gt;
&lt;p&gt;I went to the upstairs access point’s web interface, but everything seemed fine. Next, I looked at the downstairs access point from my phone, and I then realized what had happened. The two access points were disagreeing on their connection state:&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Screenshot of the Netgaer Orbi interface, showing Backhaul Status as Disconnected&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;999&quot; height=&quot;471&quot; src=&quot;https://notnite.com/_astro/orbi-disconnected.Dd4nDEXo_Ze54Km.webp&quot;&gt;&lt;/p&gt;
&lt;p&gt;I tried moving the access point, factory resetting it, replacing the cables, but nothing worked. At this point, I considered the hardware dead. For a brand with the marketing line “no more dead zones or dropped conections”, I would say that my entire upstairs floor became both of those.&lt;/p&gt;
&lt;p&gt;I could still reach the downstairs access point, but it was comparatively slow, reducing my network speed from 300 Mbps to about 20. I coordinated a plan with my dad, and we decided that we would replace the WiFi setup entirely.&lt;/p&gt;
&lt;p&gt;I looked through a lot of consumer options, but every brand had its own downsides, and I wasn’t sure what to go with. Jokingly, I suggested going with UniFi, as I had always been interested in UniFi but never had the justification to replace my current hardware. To my surprise, he agreed to my idea, so I started researching. Here goes a huge rabbithole!&lt;/p&gt;
&lt;h2 id=&quot;worlds-dumbest-woman-vs-networking-equipment&quot;&gt;World’s dumbest woman vs. networking equipment&lt;/h2&gt;
&lt;p&gt;I am going to start this post with a clear statement: I am very bad at networking. On the very first server I ran, two of my friends had root access in case I broke WireGuard again. There was also a time I tried adding a second static IP to my server and I brought it down for three hours.&lt;/p&gt;
&lt;p&gt;I don’t have a lot of freedom to play around with my homelab’s network for a few reasons:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;I am unemployed.&lt;/li&gt;
&lt;li&gt;My homelab network is actually just my home network. There’s pretty much no isolation from the rest of my network, so if I break something in a bad way, I break my residential connection and my family members get mad at me.&lt;/li&gt;
&lt;li&gt;I made the terrible mistake of hosting “important” services on a home server. Whoops!&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;However, beyond that, I vaguely know how these things work. I was focused on replacing my access point(s), but the entire selling point of UniFi is that it’s &lt;em&gt;unified&lt;/em&gt;, so I also would want their router and at least one switch.&lt;/p&gt;
&lt;p&gt;Usually, I would be annoyed about having to replace every single component in my tech stack to have a unified ecosystem (&lt;a href=&quot;https://www.apple.com/&quot;&gt;&lt;em&gt;cough&lt;/em&gt;&lt;/a&gt;), but I was actually pretty happy about having a reason to replace everything. The current switch I have downstairs is extremely old and slow, and the current router/firewall I have is a early-2000s Dell OptiPlex running pfSense.&lt;/p&gt;
&lt;p&gt;It took a bit of research and going back and forth to decide, but I eventually settled on the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;a href=&quot;https://store.ui.com/us/en/category/all-cloud-gateways/products/ux7&quot;&gt;UniFi Express 7&lt;/a&gt; as my “Cloud Gateway”, which is the fancy UniFi branding for the central device. This acts as my router, firewall, and main hub for controlling everything.&lt;/li&gt;
&lt;li&gt;Two &lt;a href=&quot;https://store.ui.com/us/en/category/switching-utility/products/usw-lite-8-poe&quot;&gt;Lite 8 PoE&lt;/a&gt;s for switching. I literally just went to the “Utility” section of their site and sorted by the minimum price. I considered the Flex, but paying $10 more for 3 more ports was worth it.&lt;/li&gt;
&lt;li&gt;A &lt;a href=&quot;https://store.ui.com/us/en/category/all-wifi/products/u7-pro&quot;&gt;U7 Pro&lt;/a&gt; for the upstairs access point. Did I &lt;em&gt;need&lt;/em&gt; WiFi 7? No. Did I get it anyways? Yes.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;My dad was currently on vacation with my brother, so he ordered it for me to pick up at a local Micro Center. My dad being on vacation was a net positive, as he was with a person who also owned UniFi, and so he was able to learn more about UniFi from direct experience. If you’re reading this, thanks John.&lt;/p&gt;
&lt;p&gt;I picked up the order from Micro Center that evening. As I walked up a set of stairs, my mom said I was carrying the bag like a newborn baby, so she decided that it would now be named Marcus. Sure, whatever, welcome to my household Marcus.&lt;/p&gt;
&lt;h2 id=&quot;initial-setup&quot;&gt;Initial setup&lt;/h2&gt;
&lt;p&gt;I originally was going to wait to replace everything until my dad got home, but the temptation of shiny toys on my living room counter combined with me running &lt;code&gt;dnf up&lt;/code&gt; at 6 MiB/s got to me. I unpacked everything and got the cables setup, and then I went downstairs to assess everything.&lt;/p&gt;
&lt;p&gt;The current equipment resides in a dilapidated corner of my garage. Everything rests on a single shelf, and the wiring was so bad that I can’t post it on this page without a content warning. A single power strip provided power for every device, with a cacophony of Ethernet cables presumably running into a switch I couldn’t easily see.&lt;/p&gt;
&lt;p&gt;This, obviously, had to go. However, a lot of this hardware was things that I did not want to touch. Removing and cleaning up everything without any help would’ve taken hours, which I didn’t want to do just yet. Instead, I decided on a temporary compromise, which I would clean up with my dad after he got home next week.&lt;/p&gt;
&lt;p&gt;While the power strip on the shelf was completely full, there was another power strip on the opposite side of the garage. I was able to bring the power strip just over to the shelf, and simply placed the UniFi equipment next to the shelf, powering it from the second strip. This was a complete mess, but it worked!&lt;/p&gt;
&lt;p&gt;Instead of being a responsible sysadmin and telling anyone about downtime, I just unplugged the Ethernet cable from my ISP’s equipment and watched as everything started to blink red. Even when I know I’m doing everything right so far, seeing all of the status lights get mad is a very uncomfortable feeling.&lt;/p&gt;
&lt;p&gt;I plugged the Ethernet cable into the Cloud Gateway, and I sat there as the cutesy screen flashed to life. I got out the UniFi mobile app, created a new account, and connected to the gateway. The network settings weren’t detected automatically (presumably because I have a static IP), so I had to manually enter the IP/gateway.&lt;/p&gt;
&lt;p&gt;And… it just worked! Seriously. I sat there in shock as it just configured everything perfectly without my input. Compared to the first time I set up pfSense, UniFi took a lot less button pressing and head scratching to get it to work. It turns out paying for convenience does work sometimes! &lt;del&gt;Maybe Apple was right.&lt;/del&gt;&lt;/p&gt;
&lt;p&gt;After the WiFi was up, I had to get all the Ethernet connections back online. I plugged in &lt;a href=&quot;https://notnite.com/blog/home-server&quot;&gt;my home server&lt;/a&gt;, Philips Hue bridge, and a few other things that were downstairs. I left my old home server (which had been unused for months) and the pfSense box sitting there, disconnected from the rest of the network, but still powered on for no reason. Is it possible I just violated the computer version of the Geneva Conventions?&lt;/p&gt;
&lt;p&gt;I fiddled around in the UI on my phone to get the HTTP ports forwarded to the server. My site was back online, after just a few minutes of downtime, so hopefully nobody noticed anything. Nice! While the other devices around the house still had to be connected, my biggest priority was complete, and I was relieved.&lt;/p&gt;
&lt;p&gt;I spent some time playing around in the UI, and I really enjoyed it. Everything seemed easy to get to and simple to configure. It’s a lot nicer than dealing with crusty NETGEAR interfaces or having to read FreeBSD logs.&lt;/p&gt;
&lt;p&gt;When I first opened the page, I saw a list of devices that were on my network, as well as the &lt;em&gt;services&lt;/em&gt; they were connecting to. Seeing the Let’s Encrypt logo inside of my router page struck deep fear into me, so I instantly turned that feature off. I presume it just inspects &lt;a href=&quot;https://en.wikipedia.org/wiki/Server_Name_Indication&quot;&gt;SNI&lt;/a&gt; or something.&lt;/p&gt;
&lt;h2 id=&quot;the-upstairs-access-point&quot;&gt;The upstairs access point&lt;/h2&gt;
&lt;p&gt;Of course, the entire reason I replaced this network was for the upstairs access point that failed. I brought the switch and access point upstairs and plugged it in on my desk. The access point started as soon as I plugged it into the switch, which was the exact moment I became a Power over Ethernet enthusiast.&lt;/p&gt;
&lt;p&gt;I looked at my phone, and I got a new notification to “adopt” the upstairs AP. I admire the term “adopt”, it could have been “claim” or “register” or “setup” or literally anything else, but instead it’s the one term that makes me feel like a proud mother. Perhaps my mom was right about naming it Marcus.&lt;/p&gt;
&lt;p&gt;Anyways, after I hit the button, it took a few minutes to configure and then I got one more prompt for the switch it was connected to. After it was finished, I went to go configure the access point, and… it was already setup???&lt;/p&gt;
&lt;p&gt;The UniFi app already set up the upstairs AP to mesh automatically, registered the switch properly, and both WiFi and Ethernet worked fine upstairs. I was very happy about this, so I ran a quick speed test on my computer, and went to collapse in bed for a bit.&lt;/p&gt;
&lt;p&gt;When I got out of bed, I walked over to my desk and saw Discord wasn’t loading. I began to wonder if &lt;a href=&quot;https://moonlight-mod.github.io/&quot;&gt;my Discord client mod&lt;/a&gt; broke, but I realized that the entire network was offline. Again.&lt;/p&gt;
&lt;p&gt;At this point, I was angry but amused at the situation. I bought into a new ecosystem expecting it to just work, only for the same exact scenario to happen once more. However, I doubted that the hardware was faulty, and I assumed that it might have just been a quirk in setup. I just rebooted the AP and walked away.&lt;/p&gt;
&lt;p&gt;When I woke up the next morning, it happened &lt;em&gt;again&lt;/em&gt;, so I began to troubleshoot. I knew that AP meshing wasn’t a very popular feature as most people just wire devices directly, so I suspected it was some issue involving that.&lt;/p&gt;
&lt;p&gt;Going to the app, I could see that the AP was marked as “Isolated”, and the upstairs switch was offline:&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Screenshot of the UniFi mobile app showing the four connected devices&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;1079&quot; height=&quot;1142&quot; src=&quot;https://notnite.com/_astro/unifi-disconnected.Cufvyi9r_1gCDDC.webp&quot;&gt;&lt;/p&gt;
&lt;p&gt;From a quick search, “isolated” apparently means that the AP was detected but not connected. This felt oddly familiar to the issue I had with the Orbi. The upstairs switch being offline makes sense, as the AP is the only way it can wirelessly communicate to the gateway.&lt;/p&gt;
&lt;p&gt;I spent a &lt;em&gt;lot&lt;/em&gt; of time investigating this and getting nowhere. I restarted the Cloud Gateway as well, but nothing really changed. I tried resetting and re-adopting the AP, but it still had the same issue. Eventually, after poking through the web UI, I found something concerning:&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Screenshot of the UniFi web UI showing Marcus meshed with itself in a tooltip&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;1080&quot; height=&quot;759&quot; src=&quot;https://notnite.com/_astro/recursive-mesh.DylC9ry7_J3TIb.webp&quot;&gt;&lt;/p&gt;
&lt;p&gt;The Cloud Gateway had meshing on, which I expected (it should be meshing to the upstairs AP). However, the tooltip showed that it wasn’t meshing to the AP, it was meshing to &lt;em&gt;itself&lt;/em&gt;. This makes absolutely no sense, and it means that something must have gone wrong in my setup - perhaps an infinite loop of some kind. Bad Marcus!&lt;/p&gt;
&lt;p&gt;I restarted the AP and Cloud Gateway, and took a look at the settings of the AP. Unfortunately, I didn’t take a screenshot of what I saw in that moment, but here’s what I see now:&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Screenshot of the UniFi web UI, with two &amp;quot;Allow Wireless Downlinks&amp;quot; and &amp;quot;Allow Wireless Uplinking&amp;quot; checkboxes&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;257&quot; height=&quot;171&quot; src=&quot;https://notnite.com/_astro/meshing-config.CmI0qg-C_ZW6d41.webp&quot;&gt;&lt;/p&gt;
&lt;p&gt;You’ll notice that, in this image, the uplinking checkbox is greyed out. This is because the device is currently uplinking to the Cloud Gateway for an Internet connection. However, when I looked at this for the first time, &lt;em&gt;both&lt;/em&gt; checkboxes were enabled and greyed out.&lt;/p&gt;
&lt;p&gt;The connection is supposed to be provided by the Cloud Gateway, which would downlink to the AP. Because of this, the AP would need to have uplinking enabled. However, it had uplinking &lt;em&gt;and&lt;/em&gt; downlinking enabled, so at one point the AP automatically reconfigured itself to reverse the flow of traffic. The Cloud Gateway would try and connect &lt;em&gt;to&lt;/em&gt; the AP for the network!&lt;/p&gt;
&lt;p&gt;This would cause the AP to get disconnected on my network, stranded as it couldn’t figure out how to connect back to the Cloud Gateway. This manifested in the AP slowly blinking blue, which indicates that there wasn’t a connection. It would eventually become “isolated” and bring down the network upstairs, but still broadcasting the SSID, causing the upstairs to turn into a network black hole.&lt;/p&gt;
&lt;p&gt;From my understanding, the UniFi app would automatically enable meshing in both directions when pairing the AP. I just had to reset and re-adopt the AP, wait for it to “get ready”, and rush to disable downlinking before it automatically kicked in. Problem solved!&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;img alt=&quot;Screenshot of the UniFi UI showing four online devices, with the AP listed with a &amp;quot;Mesh&amp;quot; uplink&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;305&quot; height=&quot;176&quot; src=&quot;https://notnite.com/_astro/unifi-devices.CVGyzzBR_ZLOEe4.webp&quot;&gt;&lt;/p&gt;
&lt;p&gt;Not the first time I’ve dealt with computers isolating, I guess.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Now that this AP was in place, I was able to connect all of the devices around the house. There were a lot of painful input methods across the various devices, but my (least) favorite was the Nest thermostat. I had to rotate the dial and press down to select a letter, one letter at a time. Who designed that???&lt;/p&gt;
&lt;h2 id=&quot;creating-vlans&quot;&gt;Creating VLANs&lt;/h2&gt;
&lt;p&gt;I wanted to get VLANs going for the house as soon as I could. I wanted to split the home devices, homelab stuff, and IoT devices into three separate networks. I didn’t want to setup any isolation rules just yet, though - my current goal was just setting up everything and configuring the devices.&lt;/p&gt;
&lt;p&gt;I created a VLAN in the UI and watched as the Cloud Gateway seemingly reconfigured itself. My status monitoring alerts detected a brief connection loss to my server. However, even though the alert said the server went back online, I didn’t have any networking upstairs. Not again…&lt;/p&gt;
&lt;p&gt;Once more, the upstairs network was broken, but the downstairs network was fine. This time, the scenario was even &lt;em&gt;more&lt;/em&gt; peculiar. I could ping addresses from my LAN and the Internet just fine, but I couldn’t SSH into my server. Confused, I went to the Cockpit web UI for my server, but it wasn’t loading. Great!&lt;/p&gt;
&lt;p&gt;I was very confused on how this happened, especially because I hadn’t even assigned any devices yet. Having a VLAN &lt;em&gt;enabled&lt;/em&gt; broke the entire network, even if it wasn’t being used at all. Disabling it fixed everything, but I obviously still wanted to use VLANs.&lt;/p&gt;
&lt;p&gt;Looking at the network tab in Firefox, it seemed like the connection was being dropped halfway through a request. This also lined up with what I saw with SSH, it would get halfway through the handshake and just stop responding. I got out my phone and got into my server with Tailscale, then tried a basic TCP connection through Termux. As expected, it just gave up after I sent a certain amount of data.&lt;/p&gt;
&lt;p&gt;I was very confused at how this happened, especially when the UniFi web page reported everything was okay. In fact, I even connected to the Cloud Gateway perfectly, but anything else on the local network or Internet was broken. I was able to see that my device was assigned an IP, but the Cloud Gateway couldn’t even tell I was making any traffic:&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Screenshot of the UniFi UI showing &amp;quot;Wired Experience&amp;quot; with empty data usage&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;387&quot; height=&quot;155&quot; src=&quot;https://notnite.com/_astro/wired-experience.DB62W9NF_ZBQNIB.webp&quot;&gt;&lt;/p&gt;
&lt;p&gt;I ended up stumbling around and found a button to download a Wireshark capture. This was really cool (and a little scary that it was so easy), so I let it run for a bit and opened it up in Wireshark:&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Screenshot of the UniFi UI showing the packet capture modal&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;431&quot; height=&quot;368&quot; src=&quot;https://notnite.com/_astro/unifi-packet-capture.DSDJz0-d_Z1OXv1s.webp&quot;&gt;&lt;/p&gt;
&lt;p&gt;Sure enough, I got a .pcap in a .tar in another .tar, and I ran &lt;code&gt;sudo dnf install wireshark&lt;/code&gt; from my mobile hotspot to take a look. One painfully slow repository update later, I opened Wireshark and saw that traffic would… indeed just eventually reset. The capture wasn’t very helpful, unfortunately.&lt;/p&gt;
&lt;p&gt;At this point, I was super confused. No amount of searching my problems would help, as I could barely find information about meshing, much less this specific problem. At this point, I had spent my entire evening working on this, and I was starting to get stressed out. I didn’t want to post on a support forum just yet, so instead I asked some friends for help.&lt;/p&gt;
&lt;p&gt;A few days ago, my friend &lt;a href=&quot;https://github.com/KazWolfe&quot;&gt;Kaz&lt;/a&gt; tested UniFi meshing for me before I bought everything, just to make sure my setup would work fine. I let him know I was having issues with VLANs, and he let me know he would take a look after he was done with his FFXIV raid night. However, he suggested I just take the AP downstairs and plug it in physically for a bit.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Screenshot of a Discord conversation where I am very upset that that suggestion worked&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;670&quot; height=&quot;199&quot; src=&quot;https://notnite.com/_astro/every-fucking-time.DEj7HT-9_Z1ChYVF.webp&quot;&gt;&lt;/p&gt;
&lt;p&gt;God damnit.&lt;/p&gt;
&lt;p&gt;I spent maybe six hours troubleshooting this, tearing my hair out trying to get this to work, changing all sorts of overrides and VLAN configs. And all I had to do was plug it in for a few minutes. Lesson learned, I guess.&lt;/p&gt;
&lt;p&gt;My assumption here is that the updated VLAN configurations weren’t being properly synced to the AP when it was meshing. Having it plugged in via Ethernet allows it to sync whatever information is required without any potential interruption. I guess I’ll see how accurate this theory is if I make a fourth VLAN in the future.&lt;/p&gt;
&lt;p&gt;Now that the VLANs actually worked, I was able to force assign each device a IP/VLAN based on its MAC address. This made it a lot easier to set up the IoT devices, since I just had to set some overrides and reconnect them.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Screenshot of the UniFi UI showing the IP Settings section, with a Virtual Network Override and Fixed IP Address setting&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;390&quot; height=&quot;305&quot; src=&quot;https://notnite.com/_astro/fixed-ip.CZdoqlBr_f3bmb.webp&quot;&gt;&lt;/p&gt;
&lt;p&gt;Some devices (like my &lt;a href=&quot;https://kno.wled.ge/&quot;&gt;WLED&lt;/a&gt; controllers) have their own static IP settings, so I didn’t specify a fixed address for those devices. I figured that if I set a fixed address in UniFi, but the device itself had a separate fixed address, they would fight over which address to force it to and cause issues.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Screenshot of the WLED UI showing the Static IP input&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;309&quot; height=&quot;261&quot; src=&quot;https://notnite.com/_astro/wled.LwRDiofy_ZV2jfs.webp&quot;&gt;&lt;/p&gt;
&lt;p&gt;Again, I’m not actually separating any traffic here. I’m using VLANs mainly for organization, not for the firewall, and the SSID isn’t forced to a specific VLAN. I eventually plan to make a second SSID for the IoT devices, but I don’t want to go around and change the connection info on everything for a &lt;em&gt;second&lt;/em&gt; time right now.&lt;/p&gt;
&lt;p&gt;In the future, I might set up strict firewall rules between everything (e.g. IoT VLAN can &lt;em&gt;only&lt;/em&gt; reach the Home Assistant IP), but that’s not very important for me at the moment. I’m happy with how it is right now, and my threat model does not include my toaster being hacked and traversing my home network (but I encourage you to try).&lt;/p&gt;
&lt;h2 id=&quot;getting-scammed-by-verizon&quot;&gt;Getting scammed by Verizon&lt;/h2&gt;
&lt;p&gt;While I was happy with my setup so far, there were two glaring issues I had with it, and both were the fault of my ISP (Verizon):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;My internet plan is currently capped by our plan’s speed. I could clearly push way more than this with the new hardware, but it’s not gonna be any faster outside of the local network.&lt;/li&gt;
&lt;li&gt;We don’t have IPv6 in the house. I live in an area that &lt;em&gt;should&lt;/em&gt; be able to get IPv6, but I never got it working with pfSense, and UniFi didn’t automatically detect IPv6 when setting everything up.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I asked my dad to recheck the pricing for the plans we were offered. I vaguely remember discussing plans with him years ago, and we decided that the faster options were too unreasonably expensive for us at the time. Here’s what we saw in our account settings:&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Screenshot of the Verizon site showing 500 Mbps for $149/mo and 1 Gig for $269/mo&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;688&quot; height=&quot;371&quot; src=&quot;https://notnite.com/_astro/verizon-plans.hNo1LE8J_iEf7a.webp&quot;&gt;&lt;/p&gt;
&lt;p&gt;That is extremely unreasonable. &lt;strong&gt;$269?&lt;/strong&gt; Seriously? To add insult to injury, we currently pay for 300/300 Mbps for $159/mo, and the 500/500 plan is $10 &lt;em&gt;cheaper&lt;/em&gt;. We were so grandfathered into a plan for so long that a cheaper option became available, and they just never told us. Sigh.&lt;/p&gt;
&lt;p&gt;I started to research, because that felt extremely wrong. Looking at &lt;a href=&quot;https://www.verizon.com/business/products/internet/fios/&quot;&gt;their marketing page&lt;/a&gt;, gigabit is… $99??? So the price they offer on their marketing page is $60 cheaper than what we’re paying now for three times the speed, and the price they show us in the account settings is over double that marketing price.&lt;/p&gt;
&lt;p&gt;I began to think that maybe the price was just different due to some quirk of my region, since I know that network plans can depend on the hardware and lines installed in your area. I put in my neighbor’s address (on the same street) onto their marketing page, and it said gigabit was available for $99. Great.&lt;/p&gt;
&lt;p&gt;I assumed this had to be something related to our current plan. Maybe this is the result of a shitty contract, or maybe our grandfathered plan is ruining our upgrade path. Perhaps there’s some hidden fee that we weren’t aware of, even though the marketing page says the price doesn’t factor in any taxes or fees.&lt;/p&gt;
&lt;p&gt;I thought that we should probably call up my ISP and ask what the hell was going on, and also maybe see if we could get IPv6 in the process. At this point, it was the end of the week, so my dad called them up when their phone line opened on Monday morning.&lt;/p&gt;
&lt;p&gt;He let me know that he tried his best to talk with the support representative, but they wouldn’t budge on gigabit’s pricing. At this point, I began foaming at the mouth with rage, ready to go start up my own NotNite ISP and fuck up BGP in the process.&lt;/p&gt;
&lt;p&gt;I don’t think this is the fault of the support rep, I’m sure they’re just following some company guideline (or maybe I’m missing something in the fine print), but I was still annoyed. Eventually, I think my dad just gave up and moved to the extremely overpriced gigabit plan. He’s paying for it, so I presume he’s fine with the price, but I still feel bad that he’s getting scammed. :(&lt;/p&gt;
&lt;h2 id=&quot;getting-scammed-by-verizon-ipv6-edition&quot;&gt;Getting scammed by Verizon (IPv6 edition)&lt;/h2&gt;
&lt;p&gt;Beyond the pricing issue, I still wanted to figure out IPv6. The support rep informed us that IPv6 should &lt;em&gt;already work&lt;/em&gt; for our plan, but I didn’t believe that for a few reasons:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;There is no information about IPv6 anywhere in the account settings. The plan specifies our static IPv4 configuration, but there’s no mention of IPv6 anywhere, not even in the plan’s description.&lt;/li&gt;
&lt;li&gt;UniFi didn’t setup IPv6 automatically when I setup the Cloud Gateway for the first time, so I presume it couldn’t find anything. It’s possible I need to configure it manually (like I did for my IPv4 address), but again, there’s no information to use.&lt;/li&gt;
&lt;li&gt;Back when I used pfSense, I tried (and failed) to get a IPv6 prefix. I doubt it would magically work now…&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I decided to turn on IPv6 in UniFi and see if it did anything automatically, but nothing happened. I tried both SLAAC and DHCPv6, changing the prefix size, but still nothing.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Screenshot of the UniFi UI showing that no IPv6 address is present&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;552&quot; height=&quot;118&quot; src=&quot;https://notnite.com/_astro/unifi-no-ipv6.dXu_WiJN_ZMJSrq.webp&quot;&gt;&lt;/p&gt;
&lt;p&gt;At this point, I started to think that either the support rep was wrong or my hardware is too old. I have no idea what model ONT I have, and to my knowledge that’s a completely different layer where it wouldn’t impact IPv6, but I considered it &lt;em&gt;could&lt;/em&gt; be the issue based on the support rep’s wording.&lt;/p&gt;
&lt;p&gt;Confused, I asked self-proclaimed IPv6 expert &lt;a href=&quot;https://maddy.kitty.garden/&quot;&gt;Maddy&lt;/a&gt; about what buttons to hit, but we couldn’t figure anything out. I also asked actually-certified IPv6 expert Kaz about this scenario, and he let me know that the most likely configuration for IPv6 would be through SLAAC. However, he noted that our account is a business plan with static addressing for IPv4, and apparently that just doesn’t work with Verizon’s IPv6?&lt;/p&gt;
&lt;p&gt;This &lt;em&gt;would&lt;/em&gt; make a lot of sense. It’s possible we needed to ask for a static IPv6 prefix on our account, since we already had a static IPv4 address, and you might not be able to mix static+dynamic plans. The support rep might not have noticed that we have a static address, which would require our plan to be reconfigured.&lt;/p&gt;
&lt;p&gt;However, we called back, and it turns out that this just &lt;em&gt;isn’t possible&lt;/em&gt;. The support reps were very confused and I guess they really just can’t do it. I think IPv6 just isn’t rolled out to business customers with static addresses - looking at &lt;a href=&quot;https://community.verizon.com/t5/Fios-Internet-and-High-Speed/IPv6-on-business-with-static-IP-take-two/m-p/1805321&quot;&gt;the forums&lt;/a&gt;, it looks like this is the case, and this post is from March of this year. Ouch.&lt;/p&gt;
&lt;p&gt;For now, I’ve given up on IPv6, even though I really want it. I hope Verizon actually makes some progress on this, since it looks like they provide IPv6 to other residential customers (and even business customers with dynamic addresses, I think?).&lt;/p&gt;
&lt;h2 id=&quot;final-thoughts&quot;&gt;Final thoughts&lt;/h2&gt;
&lt;p&gt;UniFi itself is great. I’m happy to have cleaned up my home network quite a bit, and the user interface seems a lot more friendly than what I was dealing with before. I also enjoy the fact that integrating new UniFi products into the household should be extremely painless, but I’m not in the market to spend any more money on networking equipment right now (lol).&lt;/p&gt;
&lt;p&gt;Speed wise, even though I’m upset about the insane pricing, I’m enjoying the new plan, even though I’ve yet to make good use of it. I was getting about 930/930 downstairs and 650/890 upstairs. As expected from mesh networking, the upstairs AP is slightly slower. Maybe I can tweak the AP’s settings in the future, but I don’t care enough right now.&lt;/p&gt;
&lt;p&gt;The upstairs AP still needs to be properly mounted somewhere. Right now it’s just laying against the side of my desk, and the switch is taking up space where I’d usually put my phone. I’m not entirely sure where I’ll mount it, especially because I need an Ethernet cable to the switch, so bringing it through my room’s wall into the hallway might be required.&lt;/p&gt;
&lt;p&gt;I still need to make some final touches on the network configuration (e.g. moving some things into VLANs), and I need to go downstairs and clean up the Cthulhu-ass cable management, but otherwise I’m satisfied with everything right now. In sysadmin tradition, let’s see how long it takes for me to hate something about it.&lt;/p&gt;</content:encoded></item><item><title>oh yeah thats good</title><link>https://notnite.com/blog/oytg</link><guid isPermaLink="true">https://notnite.com/blog/oytg</guid><description>My favorite video games that I&apos;ve played</description><pubDate>Wed, 05 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;These are games that I like, and I think you might like them too. This page is a rewrite of the original page, which was almost 3 years old… now just a few sentences per game instead of entire paragraphs.&lt;/p&gt;
&lt;h2 id=&quot;adventure&quot;&gt;Adventure&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://store.steampowered.com/app/420530/OneShot/&quot;&gt;OneShot&lt;/a&gt;: a puzzle/adventure game about the fourth wall and a cat who likes pancakes. Also see the remastered &lt;a href=&quot;https://store.steampowered.com/app/2915460/OneShot_World_Machine_Edition/&quot;&gt;World Machine Edition&lt;/a&gt;!&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://store.steampowered.com/app/753640/Outer_Wilds/&quot;&gt;Outer Wilds&lt;/a&gt;: adventure game about space and time. Go into this blind, spoilers ruin the game!&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://store.steampowered.com/app/253230/A_Hat_in_Time/&quot;&gt;A Hat in Time&lt;/a&gt;: cute silly platformer with a lot of DLC content. Now mostly dead, RIP.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://store.steampowered.com/app/572890/Pikuniku/&quot;&gt;Pikuniku&lt;/a&gt;: a red ball overthrows capitalism. Short and enjoyable with a silly co-op mode.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://store.steampowered.com/app/837470/Untitled_Goose_Game/&quot;&gt;Untitled Goose Game&lt;/a&gt;: be a goose that ruins everyone’s day. Also kind of a puzzle game?&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://store.steampowered.com/app/250260/Jazzpunk_Directors_Cut/&quot;&gt;Jazzpunk&lt;/a&gt;: fever dream video game. Not really sure how to explain it. Just have fun.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://store.steampowered.com/app/1703340/The_Stanley_Parable_Ultra_Deluxe/&quot;&gt;The Stanley Parable: Ultra Deluxe&lt;/a&gt;: remake of The Stanley Parable with more stuff in it, which gets quite absurd at times. It’s more than it seems.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://store.steampowered.com/app/303210/The_Beginners_Guide/&quot;&gt;The Beginner’s Guide&lt;/a&gt;: incredibly moving walking simulator about creative drive. I play it every year or so to reflect on it.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://store.steampowered.com/app/383870/Firewatch/&quot;&gt;Firewatch&lt;/a&gt;: the game that everyone used as wallpapers that one time. Beautiful mystery game that makes me feel like I go outside more than I really do.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://store.steampowered.com/app/1332010/Stray/&quot;&gt;Stray&lt;/a&gt;: you’re a cat in a dystopian city. Made me cry.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://store.steampowered.com/app/275850/No_Mans_Sky/&quot;&gt;No Man’s Sky&lt;/a&gt;: explore space! The best comeback story in the gaming industry. It’s fun to pretend I’m a space voyager.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://store.steampowered.com/app/1665500/Hypnagogia__Boundless_Dreams/&quot;&gt;Hypnagogia: Boundless Dreams&lt;/a&gt;: walking simulator about exploring dreams. I want to make a game like this!&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;platformer&quot;&gt;Platformer&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://store.steampowered.com/app/504230/Celeste/&quot;&gt;Celeste&lt;/a&gt;: beautiful slick platformer with a pretty good story, tons of extra contant, and a &lt;a href=&quot;https://everestapi.github.io/&quot;&gt;very talented modding community&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://store.steampowered.com/app/683320/GRIS/&quot;&gt;GRIS&lt;/a&gt;: beautiful artistic classic. Also play &lt;a href=&quot;https://store.steampowered.com/app/2420660/Neva/&quot;&gt;Neva&lt;/a&gt;!&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://store.steampowered.com/app/70300/VVVVVV/&quot;&gt;VVVVVV&lt;/a&gt;: classic platformer about bending gravity that came out when I was 4. Still fun to this day!&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://store.steampowered.com/app/1353230/Bomb_Rush_Cyberfunk/&quot;&gt;Bomb Rush Cyberfunk&lt;/a&gt;: funky skating game that feels like Jet Set Radio 3. Also check out &lt;a href=&quot;https://sloppers.club/&quot;&gt;my multiplayer mod&lt;/a&gt;!&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://store.steampowered.com/app/406150/Refunct/&quot;&gt;Refunct&lt;/a&gt;: extremely short and sweet platformer speedgame thing. Not sure how to describe it. Fun and mindless.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://store.steampowered.com/app/811870/Verlet_Swing/&quot;&gt;Verlet Swing&lt;/a&gt;: swing around with a rope. It’s like the swingset in elementary school all over again.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;puzzle&quot;&gt;Puzzle&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://store.steampowered.com/app/620/Portal_2/&quot;&gt;Portal 2&lt;/a&gt;: my favorite puzzle game, and speedgame of choice. Probably the patient zero of modern internet humor.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://store.steampowered.com/app/473950/Manifold_Garden/&quot;&gt;Manifold Garden&lt;/a&gt;: absolutely mind bending puzzle game that breaks most laws of reality.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://store.steampowered.com/app/1049410/Superliminal/&quot;&gt;Superliminal&lt;/a&gt;: abuse the power of depth perception to also break most laws of reality.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://store.steampowered.com/app/1003590/Tetris_Effect_Connected/&quot;&gt;Tetris Effect: Connected&lt;/a&gt;: incredibly artsy version of Tetris and also probably the best Tetris Guideline-adhering game.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://store.steampowered.com/app/702670/Donut_County/&quot;&gt;Donut County&lt;/a&gt;: consume an entire house via a hole in the ground. Somehow oddly satisfying.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://store.steampowered.com/app/3480/Peggle_Deluxe/&quot;&gt;Peggle Deluxe&lt;/a&gt;: fever dream pachinko. They should release Peggle 2 on Steam.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;action&quot;&gt;Action&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://store.steampowered.com/app/1237970/Titanfall_2/&quot;&gt;Titanfall 2&lt;/a&gt;: the best FPS campaign I’ve ever played, with an active modding community &lt;a href=&quot;https://northstar.tf/&quot;&gt;making custom servers&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://store.steampowered.com/app/1229490/ULTRAKILL/&quot;&gt;ULTRAKILL&lt;/a&gt;: the boomer shooter of the 21st century. It’s stylish as hell and there’s a ton of content.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://store.steampowered.com/app/2515020/FINAL_FANTASY_XVI/&quot;&gt;FINAL FANTASY XVI&lt;/a&gt;: they made a Final Fantasy game that’s just Devil May Cry in disguise. It’s very good, though. Haven’t actually beaten it yet because the climax of the story was so good I had to stop playing.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://store.steampowered.com/app/322500/SUPERHOT/&quot;&gt;SUPERHOT&lt;/a&gt;: time moves when you move. You’ve probably already played it but it’s fun to revisit every once in a while.&lt;/li&gt;
&lt;li&gt;Every single Half-Life game. I’m not even linking this you just know it’s good already. Alyx too.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;automation&quot;&gt;Automation&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://store.steampowered.com/app/526870/Satisfactory/&quot;&gt;Satisfactory&lt;/a&gt;: an easy way to make me waste two weeks. Extremely delightful every step of the way, but performance hits hard sometimes.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://store.steampowered.com/app/427520/Factorio/&quot;&gt;Factorio&lt;/a&gt;: the classic automation game. Never really clicked for me, though…&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;misc-multiplayer-titles&quot;&gt;Misc. multiplayer titles&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://store.steampowered.com/app/3146520/WEBFISHING/&quot;&gt;WEBFISHING&lt;/a&gt;: fish with your friends! Also check out &lt;a href=&quot;https://github.com/NotNite/GDWeave&quot;&gt;my modloader&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://store.steampowered.com/app/2132850/Rabbit_and_Steel/&quot;&gt;Rabbit and Steel&lt;/a&gt;: imagine if FFXIV raiding was good and also 2D and also you were a rabbit. Easy to pick up and tons of fun, with harder raid tiers for the sweats.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://store.steampowered.com/app/2768430/ATLYSS/&quot;&gt;ATLYSS&lt;/a&gt;: fun RPG best experienced with friends. The #1 use for &lt;a href=&quot;https://help.steampowered.com/en/faqs/view/1150-C06F-4D62-4966&quot;&gt;Steam Private Games&lt;/a&gt; worldwide.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://store.steampowered.com/app/2835570/Buckshot_Roulette/&quot;&gt;Buckshot Roulette&lt;/a&gt;: fun party game where you take turns blowing your head off. There’s some amount of strategy to it but I’m kinda stupid so I don’t know how to play it well.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://store.steampowered.com/app/464350/Screeps_World/&quot;&gt;Screeps: World&lt;/a&gt;: a programmer MMORPG where you write code to interact with the game world. I get really invested in it for like two days then drop it for a year.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://store.steampowered.com/app/632360/Risk_of_Rain_2/&quot;&gt;Risk of Rain 2&lt;/a&gt;: extremely fun roguelike. Hopefully Gearbox hasn’t fucked it up more since I wrote this line.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://xonotic.org/&quot;&gt;Xonotic&lt;/a&gt;: the free and fast arena shooter. We play this every once in a while in my Discord server.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;other-stuff-i-couldnt-sort-into-a-category-because-im-lazy&quot;&gt;Other stuff I couldn’t sort into a category because I’m lazy&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://na.finalfantasyxiv.com/&quot;&gt;FINAL FANTASY XIV Online&lt;/a&gt;: MMORPG that I’ve spent too much time writing mods for. it gets better after 300 hours. My only exception to the no-live-service rule in this post.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://store.steampowered.com/app/1262350/SIGNALIS/&quot;&gt;SIGNALIS&lt;/a&gt;: survival horror with an incredible story. Yay lesbians!&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://store.steampowered.com/app/2379780/Balatro/&quot;&gt;Balatro&lt;/a&gt;: it’s gambling. It’s gambling. It’s just gambling. It’s peak.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://store.steampowered.com/app/2365810/Pseudoregalia/&quot;&gt;Pseudoregalia&lt;/a&gt;: a metroidvania with incredible movement. The game ends very abruptly, though, so I have no idea what the story is about.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://store.steampowered.com/app/1167630/Teardown/&quot;&gt;Teardown&lt;/a&gt;: voxel physics sandbox where you can destroy anything. RIP that one flying exploit with the wooden boards.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://store.steampowered.com/app/270880/American_Truck_Simulator/&quot;&gt;American Truck Simulator&lt;/a&gt;: trucking simulator. If you speed fast enough, the early bonus offsets the traffic violations.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://store.steampowered.com/app/1672970/Minecraft_Dungeons/&quot;&gt;Minecraft Dungeons&lt;/a&gt;: Diablo but Minecraft. It’s actually pretty fun.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://store.steampowered.com/app/1299460/Wanderstop/&quot;&gt;Wanderstop&lt;/a&gt;: a “cozy game” about tea, burnout, and change. Incredible writing, music, atmosphere, art, and story.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;honorable-mentions&quot;&gt;Honorable mentions&lt;/h2&gt;
&lt;p&gt;These are games that I haven’t finished yet (and have been meaning to for years…), but you might be interested in them!&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://store.steampowered.com/app/2078510/VIVIDLOPE/&quot;&gt;VIVIDLOPE&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://store.steampowered.com/app/1772830/Rusted_Moss/&quot;&gt;Rusted Moss&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://store.steampowered.com/app/1388770/Cruelty_Squad/&quot;&gt;Cruelty Squad&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://store.steampowered.com/app/1533420/Neon_White/&quot;&gt;Neon White&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://store.steampowered.com/app/553420/TUNIC/&quot;&gt;TUNIC&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://store.steampowered.com/app/257850/Hyper_Light_Drifter/&quot;&gt;Hyper Light Drifter&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>PlayerScope, plugins, and Dalamud&apos;s fate</title><link>https://notnite.com/blog/playerscope</link><guid isPermaLink="true">https://notnite.com/blog/playerscope</guid><description>What awaits us in Pandora&apos;s Box?</description><pubDate>Sat, 11 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://notnite.com/blog/gshade-tango-2&quot;&gt;When I “fought” GShade almost two years ago&lt;/a&gt;, a lot of my work in that timeframe made its waves across the FFXIV community. I had learned firsthand what it felt like to be capital-letter Known, and also what it felt like to have a lot of your friends mad at you. While my friend groups cheered me on and random community members praised my work, the Dalamud maintainers and moderators I was friends with weren’t as happy.&lt;/p&gt;
&lt;p&gt;I was booted off of the Discord server’s support team, and the server banned discussion of the current events completely. This was a really harsh reality for me to face - I had derived a lot of individual meaning from my ability to help (especially on chaotic patch days), and my attachment to the project in any form was eliminated right then and there. This led me down a difficult few months when my respect was earned in the wrong place I wanted it - I wanted my peers to trust me, not random people on the internet.&lt;/p&gt;
&lt;p&gt;I’m a different person now, with a better outlook on my drive for creation, and I’ve rebuilt most of my trust with those people. I have since learned to not do what makes me feel like I’m “part of a community”, and instead focus on what makes me and others happy the most. I’ve mostly stopped contributing to Dalamud and XIVLauncher, and I put my attention towards building other software that makes people happy (with notable examples being &lt;a href=&quot;https://sloppers.club/&quot;&gt;Slop Crew&lt;/a&gt;, &lt;a href=&quot;https://moonlight-mod.github.io/&quot;&gt;moonlight&lt;/a&gt;, and &lt;a href=&quot;https://github.com/NotNite/GDWeave&quot;&gt;GDWeave&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;While I’ve had &lt;a href=&quot;https://github.com/jawslouis/MakePlacePlugin/pull/26&quot;&gt;a few other moments&lt;/a&gt; since, I learned to quiet down and not stir up unneeded drama. I took the impression from the scorched-earth policy on discussion that most Dalamud maintainers did not want to poke at the community’s monthly drama.&lt;/p&gt;
&lt;p&gt;Many, &lt;em&gt;many&lt;/em&gt; things would pass since then. Despite all that time, we’re seeing another one of those moments - a somewhat-unified dislike towards a project’s goal, stirring up enough discourse to &lt;a href=&quot;https://www.pcgamer.com/games/final-fantasy/final-fantasy-14-communities-panic-as-it-turns-out-change-to-blacklisting-meant-to-help-reduce-stalking-also-lets-players-use-mods-to-track-their-alts/&quot;&gt;make it to PCGamer&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The project is known as “PlayerScope”, a custom repository Dalamud plugin, and the goal of it is to correlate alts of players using an account identifier sent to the client. This would catch me off guard &lt;a href=&quot;https://dalamud.dev/news/2025/01/10/account-ids-and-plugins&quot;&gt;seeing the Dalamud maintainers publish a statement on it&lt;/a&gt;, which means things have gotten &lt;em&gt;truly&lt;/em&gt; dire, in my eyes. This is a stark contrast to the handling of the GShade incident - of course, brought upon from the fact that it’s more relevant to Dalamud itself - but still unusual for their patterns of communication.&lt;/p&gt;
&lt;p&gt;This feels oddly familiar to everything I witnessed two years ago, except now this has a direct privacy issue tied into it. The stakes are much higher, and I watch over this kind of turmoil - one not focused on a group of players’ actions (&lt;a href=&quot;https://mogtalk.org/2024/12/03/the-ffxiv-world-race-and-its-future/&quot;&gt;like with most drama&lt;/a&gt;), but rather a piece of software itself - and I fear for the future of our ecosystem.&lt;/p&gt;
&lt;h2 id=&quot;how-did-we-get-here-anyway&quot;&gt;How did we get here, anyway&lt;/h2&gt;
&lt;p&gt;For the readers here that somehow do not know what I’m talking about - meet &lt;a href=&quot;https://goatcorp.github.io/&quot;&gt;XIVLauncher&lt;/a&gt;. XIVLauncher is a custom launcher for FFXIV, most known for the in game plugin system named &lt;a href=&quot;https://github.com/goatcorp/Dalamud&quot;&gt;Dalamud&lt;/a&gt;. Dalamud is considered a third party tool, and third party tools are against the terms of service for FFXIV. Despite this, there are a &lt;em&gt;lot&lt;/em&gt; of Dalamud users. As I write this, over 20,000 people are connected to &lt;a href=&quot;https://github.com/Penumbra-Sync/client&quot;&gt;Mare Synchronos&lt;/a&gt; (a project I’ll talk about in a few paragraphs).&lt;/p&gt;
&lt;p&gt;When I started playing FFXIV, sometime around the release of Shadowbringers, I was told by the friend that got me into this game about this cool launcher that adds some quality of life features. I was explicitly told to not talk about this in game - it being against the terms of service and all - and moved on with it. Dalamud was decently well known back then, but not at the numbers that we know today. (For the record, nobody knows of the actual number of Dalamud users minus the maintainers.)&lt;/p&gt;
&lt;p&gt;A bit before my time (I want to say 2020 or so), Dalamud was very simple. Zip files of plugins were submitted to &lt;em&gt;the&lt;/em&gt; repository (there were no custom repositories), and the concept of “dev plugins” was a folder you copied DLLs into. Plugins were simpler, with us holding less understanding of the game client and jankier APIs to use. (&lt;a href=&quot;https://github.com/goatcorp/SamplePlugin/tree/7382a8ed3623bd7013839f560c40a52f25bb754a/UIDev&quot;&gt;Does anyone else remember UIDev?&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;Dalamud would &lt;a href=&quot;https://github.com/goatcorp/Dalamud/commit/4ca76180b745f7fc7d82bc9544fe8310311d4db6#diff-3cfdc376116ffab373dda03ee563c13bf2ce9a497cdce39d989283814aeacee6R188&quot;&gt;add custom repositories&lt;/a&gt; sometime later, allowing developers to host their own plugin lists without having to get direct approval from the Dalamud team. I like how this system works (I even took the idea for &lt;a href=&quot;https://moonlight-mod.github.io/&quot;&gt;my Discord client mod&lt;/a&gt;), but it also inherently opens a hole for malicious actors to make plugins with questionable morals. Custom repositories play an important part in this story, as a lot of plugins that get the attention of users (like &lt;a href=&quot;https://github.com/xivdev/Penumbra&quot;&gt;Penumbra&lt;/a&gt;) are distributed on a custom repository. Nowadays, they are essential to learn how to use for anyone wanting to mod assets.&lt;/p&gt;
&lt;p&gt;The unfortunate truth is that these custom repositories have been boiled down into simple guides so much that some users don’t realize they’re even different. From my time providing support in the Dalamud Discord server, a fair share of users didn’t even understand where some of their plugins came from, or what we meant by “disabling third party plugins”. We’d eventually adopt the term “custom repository” instead, because people would argue that Dalamud itself is technically third party to the game.&lt;/p&gt;
&lt;p&gt;Nowadays, people will hear about popular plugins, want to install them, and follow an installation guide that dumbs down all the security risks they should be thinking critically about. Dalamud plugins are just code, and they can do anything on your computer. Some users don’t care, though - they just wait the 15 or so seconds for the prompt to go away, and add a URL that downloads and executes unverified code on their machine.&lt;/p&gt;
&lt;p&gt;Most users I’ve seen do not realize that they need to trust the developer of the plugins they install. For the official repository, this is fine because &lt;a href=&quot;https://github.com/goatcorp/Plogon&quot;&gt;every plugin is audited and built from source&lt;/a&gt; (I helped build that!), but custom repositories can be made by anyone, and the deployed binaries don’t even have to match the published source code of the plugin. This isn’t an issue for the big name plugins from trusted developers, but the act of adding a repository is so trivial that it’s hard to remember you have to trust the developer of every single URL you add.&lt;/p&gt;
&lt;p&gt;The community has dumbed down being able to run arbitrary code too much, in an attempt to optimize away having to answer questions. Countless &lt;a href=&quot;https://www.youtube.com/results?search_query=ffxiv+penumbra+install&quot;&gt;YouTube videos&lt;/a&gt; and &lt;a href=&quot;https://reniguide.carrd.co/&quot;&gt;crusty looking Carrds&lt;/a&gt; serve as shorthands to be linked to - a codex of every possible question and issue one could have when installing custom repository plugins, in an attempt to be accessible.&lt;/p&gt;
&lt;p&gt;Should we really be trying to optimize for clueless users? Should we really be trying to focus our efforts on user accessibility at all costs when the users in question don’t know what they’re getting into? Some may call this gatekeeping, but I firmly believe that we shouldn’t be trying to build software this way. The patterns and behavior we establish as common place for these users matter, and the imprint we leave on how to interact with Dalamud and what is “safe” is extremely important. There is a bar that has to be set somewhere.&lt;/p&gt;
&lt;h2 id=&quot;the-dead-practice-of-staying-hidden&quot;&gt;The dead practice of staying hidden&lt;/h2&gt;
&lt;p&gt;Over time, the number of Dalamud users concerningly grew. In the past, most people used common sense and didn’t talk about their use of plugins in game, or linked their online modding persona to their in game character information. I do not think the community has this skill anymore!&lt;/p&gt;
&lt;p&gt;As time went on and Dalamud became more popular, people started doing the unthinkable and &lt;em&gt;talking about plugins in game chat.&lt;/em&gt; Hell, half of the North American Party Finder experience is linking your &lt;a href=&quot;https://tomestone.gg/&quot;&gt;Tomestone.gg&lt;/a&gt; profile when forming a party nowadays. We as a community have developed a dangerous habit of announcing a bannable offense right where it can be recorded.&lt;/p&gt;
&lt;p&gt;Most people I ask about this say that it’s okay if it’s in a &lt;code&gt;/tell&lt;/code&gt; or Free Company chat, but I strongly disagree. The fact that you can or can’t be reported does not mean a lot when Square Enix has already logged you admitting to breaking the terms of service. I have seen a lot of people just directly not care about this, and I struggle to understand why.&lt;/p&gt;
&lt;p&gt;I’ve talked to a lot of friends I know who are guilty of this, and I have yet to hear a coherent reason to justify willingly “video-game-TOS-incriminating” yourself. A common argument I hear is “I wouldn’t care if I got banned”, which must be some sort of denial given the people I talked to have sunk several thousand hours into this game.&lt;/p&gt;
&lt;p&gt;I largely attribute one of the big players in this change in mindset to &lt;a href=&quot;https://github.com/Penumbra-Sync/client&quot;&gt;Mare Synchronos&lt;/a&gt;. For the unaware, Mare is a plugin that enables you to sync your character mods &lt;em&gt;in real time&lt;/em&gt; with other players. You can enable a mod or change your outfit clientside, and it will sync to every other person you’ve “paired” with in real time. (This is also a very concerning security issue given that the file parsers for this game are not good, but that’s a discussion for another day.)&lt;/p&gt;
&lt;p&gt;Mare (and the greater asset modding scene) is a very big draw towards this part of the ecosystem, given the infinite customization options it awards you, and being able to show those options to your friends. The official instructions for Mare has always been to only pair with who you trust, but again, users do not care. Mare features a system called “syncshells” which allow you to sync an entire group together at once. Of course, this would quickly evolve beyond the original scope of a close group of friends, now being used at scale for large in-game gatherings and codes shared into the public. (Syncshells are also dangerous for the above mentioned file parsers, but again, another day.)&lt;/p&gt;
&lt;p&gt;This kind of feature invites a culture that is hard to control. It is designed to allow you to announce your character’s presence with mods to a large group of people, which already reaches beyond the original commonly-known statement of “don’t talk about plugins in game”. When joining a syncshell, the act of you using mods can be verified by anyone else in that syncshell. Given the nature of the system, other friends will get invited, and who knows about your character and your mods spirals out of control.&lt;/p&gt;
&lt;p&gt;Some Mare players I’ve seen also directly advertise themselves - they disregard all safety warnings given out in the community, and will advertise their use of Mare in their Search Info or otherwise in game chat. The usual play is some auto-translate entry dogwhistle that leads into a &lt;code&gt;/tell&lt;/code&gt; to ask for their pairing code. I strongly believe that this is incredibly unsafe, and the fact we’ve gotten here is very disappointing to me.&lt;/p&gt;
&lt;p&gt;Some people don’t want to maintain this threat model anymore, probably because they’ve gotten too comfortable with the idea that nobody will get banned. I usually hear that “it’s such a tiny user base, they don’t care” or “it’s such a large user base, if they killed it everyone would stop playing”. I disagree with both. Penumbra 1.3.2.0, released last month, has &lt;a href=&quot;https://api.github.com/repos/xivdev/penumbra/releases/190685236&quot;&gt;&lt;strong&gt;over 100,000 downloads&lt;/strong&gt;&lt;/a&gt; - that’s a very big portion of the population &lt;a href=&quot;https://luckybancho.ldblog.jp/wsurvey.htm?world=Global&quot;&gt;using scraped Lodestone estimations&lt;/a&gt;! In my opinion, Square Enix isn’t going to hesitate to try and stop it if they wanted, just like they still shuffle packet opcodes to this day to break ACT.&lt;/p&gt;
&lt;p&gt;We have hundreds of thousands of active players running around using third party tools, some of them clueless enough to admit it, and some of them conditioned to automatically trust the plugins they see. What next?&lt;/p&gt;
&lt;h2 id=&quot;miqote-and-mouse&quot;&gt;Miqo’te and mouse&lt;/h2&gt;
&lt;p&gt;Back to the original point of this blog post - PlayerScope. In April, Square Enix announced that they were changing the blacklist system. Some technical limitations they mentioned caused a few of us in the Dalamud community to wonder if they were going to implement it clientside - I even &lt;a href=&quot;https://bsky.app/profile/did:plc:ra3gxl2udc22odfbvcfslcn3/post/3lfgbgcrcv22y&quot;&gt;called this myself&lt;/a&gt; at the time.&lt;/p&gt;
&lt;p&gt;Come 7.0, a plugin developer realizes that there are some new fields on the player game object. After an investigation, we find that these are the player Content ID and Account ID.&lt;/p&gt;
&lt;p&gt;The “Content ID” is a unique ID for your player. It starts at a very large base (depending on the region you created your character in) and increments for each character that is created. I used this technique to estimate that &lt;a href=&quot;https://twitter.com/NotNite/status/1729322316440416551&quot;&gt;the Cloud Test datacenter had 76,000 characters created on it&lt;/a&gt;. We’ve had ways to see a player’s Content ID before, but never this easily.&lt;/p&gt;
&lt;p&gt;The “Account ID”, however, is different. This is tied to your FFXIV &lt;em&gt;service account&lt;/em&gt;, so multiple characters on the same service account will have the same account ID. We’ve almost never been able to see this account ID in the network before, so having everyone’s account ID attached to their character is a big one.&lt;/p&gt;
&lt;p&gt;Both of these IDs are not that useful for anything beyond identification (you can’t use them to log into your account or anything). They’re still fun to stare at (especially with the region data being exposed), and I ended up writing a quick proof of concept plugin to dump the starting region of a character at the launch of 7.0.&lt;/p&gt;
&lt;p&gt;This, of course, allows a dangerous deanonymization technique. If you scrape enough players, you can correlate their account IDs together, and detect alts of players. That’s where PlayerScope comes in - it does that! Of course, this can only be done with crowdsourcing or botting, and the plugin relies on the former. Part of the reason I’m so annoyed at this news cycle for how “dangerous” the plugin is comes from the fact that the plugin relies on it being known and used. By writing this, I am of course getting that knowledge out more, which I recognize myself and apologize for.&lt;/p&gt;
&lt;p&gt;For the record, PlayerScope is distributed in a custom plugin repository. Even if the Dalamud team doesn’t approve of the plugin idea, users have developed the habit to automatically trust the repository, and they know how to find and enable it. This is an unfortunate series of events based on the fact that we taught the general public how to use these repositories to get to Penumbra/Glamourer/Mare, and they wouldn’t have been accepted (or wouldn’t have been submitted) to the official repository.&lt;/p&gt;
&lt;p&gt;I don’t really care about the project itself - this was going to happen eventually - but what concerns me is the public’s reaction. The &lt;a href=&quot;https://dalamud.dev/news/2025/01/10/account-ids-and-plugins&quot;&gt;blog post from Dalamud maintainers&lt;/a&gt; says “Any tool capable of reading game data (e.g. Cheat Engine) or sniffing network data (e.g. ACT, Wireshark) is able to grab and extract these values” - which is &lt;em&gt;true&lt;/em&gt;, but it undermines the validity of packet capturing for the sake of reverse engineering or archival. This has put a lot of people in panic about what data is being sent or received from the server, which worries me for the future reception to archival projects.&lt;/p&gt;
&lt;p&gt;Of course, this now has the community asking “why can’t Dalamud just stop PlayerScope?”. There’s a lot of ways to answer this question, but the easiest one to explain is that you can’t really stop someone who’s dedicated. Dalamud is completely open source, and it would be trivial to bypass any “block”, which would just evolve into a cat and mouse game. While I was able to fight it with GShade, Dalamud developers do not have the time or immaturity to do that, and the events of GShade were only in my favor because I was dealing with an irresponsible developer.&lt;/p&gt;
&lt;p&gt;This is what I meant earlier by custom repositories and questionable morals. This openness in a community is a good thing, but bad actors will always exist. If this outlet did not exist for them, they would fork Dalamud or build another tool. There’s also questions about why plugins are able to even see that data, and that’s because it ends up in memory which we have full control over - attempts to sandbox plugin APIs and block free memory access have been proposed, but none have gone through, and likely never will due to how much flexibility it removes for plugin developers.&lt;/p&gt;
&lt;p&gt;Last I’ve heard of PlayerScope, the developer “really appreciate(s) the interest” and is working to make it an actual public plugin (this entire drama came from a private version of the plugin!). The developer’s identity is mostly unknown - the GitHub account seems brand new, but the username seems to have matches on other sites, particularly on MyAnimeList where the account has marked Hitler as a “Favorite Person”. Jesus Christ.&lt;/p&gt;
&lt;h2 id=&quot;how-this-could-end&quot;&gt;How this could end&lt;/h2&gt;
&lt;p&gt;This entire scenario is a sad intersection between the normalization of third party tools and how much they annoy Square Enix. The exploit is the fault of Square Enix’s, in my opinion, but they probably won’t care about fixing the blacklist system. What they &lt;em&gt;will&lt;/em&gt; do is unknown, but there are a few options I can envision:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The plugin dies down and nobody cares anymore.&lt;/li&gt;
&lt;li&gt;Square Enix kills Dalamud in some way. They have many ways to do this, from adding client-side anti-cheat, to adding some basic obfuscation in their build system, to turning on a compiler option that’ll ruin our work. There are several ways to kill Dalamud that doesn’t involve anti-cheats, though!&lt;/li&gt;
&lt;li&gt;Square Enix fixes the blacklist system, treating it as an exploit. I find this unlikely, given that the exploit is the result of a third party tool, and it’s likely more efficient for them to just kill the tool.&lt;/li&gt;
&lt;li&gt;The developer continues maintaining the plugin, and it gains popularity. Like other third party tools, it is mentioned carelessly in chat, furthering the infestation of third party tools into Square Enix’s userbase.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Square Enix does not like third party tools - &lt;a href=&quot;https://na.finalfantasyxiv.com/lodestone/topics/detail/436dce7bd078c914009957f2221c13e6a5cb497d&quot;&gt;their statement from TOP&lt;/a&gt; should tell you that enough. Their stance has always remained fairly neutral, though. Most of their efforts go towards tools they don’t like existing (e.g. opcode shuffling to make ACT harder). They have expressed not wanting to add invasive software into FFXIV in an attempt to detect third party tools, and the most the client really has for anticheat is ACLs and a check for debuggers.&lt;/p&gt;
&lt;p&gt;The more drama we cause and the more things Square Enix has to deal with, the worse. We do not know what their threshold is, and none of us certainly want to find out. This is what makes me so upset about this entire discourse - we have slowly normalized the use of plugins in FFXIV, and now we must pay for it, as each time we slowly interfere more and more with Square Enix’s game.&lt;/p&gt;
&lt;p&gt;I spent several years of my life dedicated towards building software for FFXIV because I love it, and I would hate for that work to vanish. I wrote this post not to specifically call out the behavior of this specific plugin (even though that’s the hook), or to say we are approaching FFXIV Doomsday, but rather to express my frustration at the state we’ve gotten ourselves into. I hope our projects live on, and I hope the state of things improves.&lt;/p&gt;</content:encoded></item><item><title>Reflecting on 2024</title><link>https://notnite.com/blog/2024</link><guid isPermaLink="true">https://notnite.com/blog/2024</guid><description>Spending too much free time reflecting on my life, now in blog form!</description><pubDate>Mon, 30 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I did a lot of things this year. In 2023, I had &lt;a href=&quot;https://github.com/NotNite?tab=overview&amp;#x26;from=2023-12-01&amp;#x26;to=2023-12-31&quot;&gt;2,556 contributions on GitHub&lt;/a&gt;, and I’ve only &lt;a href=&quot;https://github.com/NotNite?tab=overview&amp;#x26;from=2024-12-01&amp;#x26;to=2024-12-31&quot;&gt;slightly undershot that&lt;/a&gt; this year. Last year, I wanted to reflect on the projects I made each year, but never got around to it. This time, I’m finally sitting down and doing it!&lt;/p&gt;
&lt;h2 id=&quot;january&quot;&gt;January&lt;/h2&gt;
&lt;p&gt;I spent New Years Day &lt;a href=&quot;https://github.com/NotNite/csgenny&quot;&gt;working on helper scripts&lt;/a&gt; for &lt;a href=&quot;https://github.com/cursey/regenny&quot;&gt;ReGenny&lt;/a&gt;, a reverse engineering tool. I had to &lt;a href=&quot;https://github.com/cursey/regenny/pull/18&quot;&gt;write a bit of C++&lt;/a&gt; to add some features into the project. I don’t write C++ a lot, so I was really nervous about doing this, but it turned out okay. It was a bit of an omen writing C++ on the first day of the year, as I had to spend a lot of time touching C++ projects this year, and I &lt;em&gt;really&lt;/em&gt; hate C++.&lt;/p&gt;
&lt;p&gt;On the 9th, I made &lt;a href=&quot;https://github.com/NotNite/htmlua&quot;&gt;htmlua&lt;/a&gt;, a shitty way to make websites with Lua. This kind of thing is demonstrated casually by people on how to build DSLs in Lua/Kotlin/similar, but I turned it into an actual library with component support. It was pretty fun to work on, but nobody’s ever actually used it. I did get &lt;a href=&quot;https://github.com/NotNite/htmlua/pull/1&quot;&gt;a spam PR&lt;/a&gt; for it once, though!&lt;/p&gt;
&lt;p&gt;On the 10th, I made &lt;a href=&quot;https://github.com/NotNite/OfDungeonsDeep&quot;&gt;OfDungeonsDeep&lt;/a&gt; (which is now maintained by &lt;a href=&quot;https://github.com/MKhayle&quot;&gt;Khayle&lt;/a&gt;), which was the first spiteware of the year. Spiteware is defined as software written for the express purpose of spite. I don’t actually like FFXIV Deep Dungeon - I just did it because I was annoyed at the developer of the existing Deep Dungeon plugin’s decisions and bugs.&lt;/p&gt;
&lt;p&gt;On the 17th, I made &lt;a href=&quot;https://github.com/NotNite/brc-save-editor&quot;&gt;a save editor for Bomb Rush Cyberfunk&lt;/a&gt;, written in React. This wasn’t a very good project, but it served as a useful reference for the WEBFISHING save editor later in October. I ended up not really liking the choice of React for this project, since I modeled the save as a class, and data binding the class with reactivity was very annoying (I ended up solving it with jank force refresh hacks that I shouldn’t have done).&lt;/p&gt;
&lt;p&gt;I also made &lt;a href=&quot;https://github.com/NotNite/ImmediateMastodon&quot;&gt;a Mastodon client that uses ImGui&lt;/a&gt; the day after (goes to show how fast I bounce around projects). This project was really unfinished and quite bad, but I ended up taking the idea further for ImSky with Bluesky.&lt;/p&gt;
&lt;p&gt;On the 25th, I made &lt;a href=&quot;https://github.com/NotNite/decky-totp&quot;&gt;decky-totp&lt;/a&gt;, a simple plugin for &lt;a href=&quot;https://decky.xyz/&quot;&gt;Decky Loader&lt;/a&gt; for the Steam Deck. I wanted to show my TOTP codes for FFXIV without having to get my phone every time I started it on my Steam Deck. While I was making this, I got upset at several aspects of the Decky ecosystem (for example, the C backend was extremely weird and unneeded).&lt;/p&gt;
&lt;p&gt;On the 27th, I made &lt;a href=&quot;https://github.com/NotNite/FirstPersonFunk&quot;&gt;a first person mod for Bomb Rush Cyberfunk&lt;/a&gt; because someone asked for it. The implementation was very janky, but it worked, and it was quite funny to play with.&lt;/p&gt;
&lt;p&gt;On the 28th, I published 0.3.0 for &lt;a href=&quot;https://github.com/NotNite/skindl&quot;&gt;skindl&lt;/a&gt;, a downloader for Bomb Rush Cyberfunk character mods. I wrote this late December because I needed a project to get used to &lt;a href=&quot;https://github.com/kata0510/Lily58&quot;&gt;my new keyboard&lt;/a&gt; with, so it doesn’t count as “made”, but I wanted to include it anyway!&lt;/p&gt;
&lt;p&gt;On the 31st, I made &lt;a href=&quot;https://github.com/NotNite/PoloTweaks&quot;&gt;PoloTweaks&lt;/a&gt;, a collection of tweaks for Bomb Rush Cyberfunk. This never went anywhere, but I wish it did - there’s a lot of fun stuff there for extending it!&lt;/p&gt;
&lt;h2 id=&quot;february&quot;&gt;February&lt;/h2&gt;
&lt;p&gt;On the 4th, I published my first blog post of the year - &lt;a href=&quot;https://notnite.com/blog/minecraft-to-goldsrc&quot;&gt;Converting Minecraft maps to Half-Life 1&lt;/a&gt;. If you haven’t read that post, go do it now! It was a blast to work on. I forgot to ever upload the map anywhere, though… Oops!&lt;/p&gt;
&lt;p&gt;On the 7th, I started work on &lt;a href=&quot;https://github.com/NotNite/Cyberhead&quot;&gt;my Bomb Rush Cyberfunk VR mod&lt;/a&gt;. Like my other mod, &lt;a href=&quot;https://sloppers.club/&quot;&gt;Slop Crew&lt;/a&gt;, this one had a lot of trouble with Thunderstore’s malware filters. Thankfully, I didn’t deal with those filters when making GDWeave in October. I ended up losing interest before getting it into a complete state, but it’s still there on Thunderstore if anyone wants to try it.&lt;/p&gt;
&lt;p&gt;On the 19th, I started playing on a friend’s Minecraft server, so I wrote a Discord to Minecraft bridge for that server. This server ended up consuming a lot of my time (even visible on my GitHub graph!), and I ended up shifting gears from Bomb Rush Cyberfunk to Minecraft for a bit.&lt;/p&gt;
&lt;p&gt;On the 23rd, I made &lt;a href=&quot;https://github.com/NotNite/enderdragon&quot;&gt;enderdragon&lt;/a&gt;, a shitpost adding Ghidra into Minecraft - my third most starred repository as of writing, but it deserves none of those compared to the top two.&lt;/p&gt;
&lt;h2 id=&quot;march&quot;&gt;March&lt;/h2&gt;
&lt;p&gt;March was a chiller month for me, in comparison. On the 6th, I &lt;a href=&quot;https://github.com/NotNite/polaris&quot;&gt;started an XMPP client&lt;/a&gt;, but it went nowhere as the protocol sucks to work on. I’d like to revisit it someday, but I’m not convinced Tauri is a good choice for it anymore.&lt;/p&gt;
&lt;p&gt;On the 17th, I made &lt;a href=&quot;https://github.com/NotNite/TemporalStasis&quot;&gt;Temporal Stasis&lt;/a&gt;, a FFXIV network proxy library. This project hasn’t been useful for a while, up until this month, where my friend &lt;a href=&quot;https://github.com/WorkingRobot&quot;&gt;Asriel&lt;/a&gt; forked it &lt;a href=&quot;https://waiting.camora.dev/&quot;&gt;to scrape FFXIV datacenter travel information&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;On the 18th, FFXIV removed 32-bit support, so &lt;a href=&quot;https://github.com/goatcorp/FFXIVQuickLauncher/pull/1451&quot;&gt;I made a PR fixing XIVLauncher for it&lt;/a&gt;. This PR was very sloppy because I was very tired, so the comments are a bit embarrassing to look at. I also got my Framework Laptop that day, which I’ve loved since - even &lt;a href=&quot;https://bsky.app/profile/did:plc:ra3gxl2udc22odfbvcfslcn3/post/3le55zi5slk2i&quot;&gt;installing a new screen for it&lt;/a&gt; this Christmas!&lt;/p&gt;
&lt;p&gt;Towards the end of the month, I wanted to add Flash games to FFXIV, but it went nowhere because I didn’t know what I was doing with &lt;a href=&quot;https://docs.rs/wgpu&quot;&gt;wgpu&lt;/a&gt;. &lt;a href=&quot;https://github.com/Styr1x/Browsingway/issues/52&quot;&gt;A feature request that would’ve saved this project&lt;/a&gt; is still open today…&lt;/p&gt;
&lt;h2 id=&quot;april&quot;&gt;April&lt;/h2&gt;
&lt;p&gt;For April Fools, I only really helped with the Dalamud team’s joke - &lt;a href=&quot;https://github.com/goaaats/horse-icons&quot;&gt;turning plugin icons into horses&lt;/a&gt;. It just involved a lot of Photoshop.&lt;/p&gt;
&lt;p&gt;On the 7th, I made up for my failed Flash game project by &lt;a href=&quot;https://github.com/NotNite/Eorzulator&quot;&gt;adding emulators into it instead&lt;/a&gt;. This code was only possible because of &lt;a href=&quot;https://github.com/karashiiro/Simulacrum&quot;&gt;karashiiro’s Simulacrum&lt;/a&gt; serving as good reference.&lt;/p&gt;
&lt;p&gt;On the 10th, I was working on a FFXIV wiki that was 100% generated through data, but it didn’t go anywhere. Probably would be easier with &lt;a href=&quot;https://github.com/ackwell/boilmaster&quot;&gt;boilmaster&lt;/a&gt; now existing.&lt;/p&gt;
&lt;p&gt;Late on the night of the 10th, I started work on an unnamed FFXIV datamining project with some friends, in preparation for the Dawntrail benchmark that would be releasing soon. If you’re one of the few that helped me with that: thanks! You guys are the best.&lt;/p&gt;
&lt;p&gt;On the 20th, I made &lt;a href=&quot;https://github.com/NotNite/benchtweaks&quot;&gt;benchtweaks&lt;/a&gt;, porting the features of some plugins to the benchmark (like removing letterboxing and a simple file replacer). It’s written in Rust, and I hated working in it, because Rust can’t do offsets in structs properly.&lt;/p&gt;
&lt;p&gt;On the 22nd, I made &lt;a href=&quot;https://github.com/NotNite/Magia&quot;&gt;Magia&lt;/a&gt;, a C# library for game modding. This never really went anywhere, but I still want to pick it up and make it fast someday. Right now it can randomly access violation for reasons I don’t understand. Whoops!&lt;/p&gt;
&lt;p&gt;On the 28th, I made GobboCam, which you can read about &lt;a href=&quot;https://notnite.com/blog/gobbocam&quot;&gt;in this blog post&lt;/a&gt;. I’ve fallen behind on posting there in recent months…&lt;/p&gt;
&lt;h2 id=&quot;may&quot;&gt;May&lt;/h2&gt;
&lt;p&gt;On the 2nd, I finally published &lt;a href=&quot;https://notnite.com/blog/framework-laptop&quot;&gt;my blog post about my Framework Laptop&lt;/a&gt;. It was sitting in my repository for months collecting dust, and I forgot to publish it until I just saw it one day when cleaning up the blog.&lt;/p&gt;
&lt;p&gt;On the 3rd, I made &lt;a href=&quot;https://github.com/NotNite/moondust&quot;&gt;moondust&lt;/a&gt;, a simple application to strip Luau type hints. This isn’t a great solution for writing typed Lua, because Luau has some extra language changes, but it’s nice for quick stuff.&lt;/p&gt;
&lt;p&gt;On the 14th, I started playing &lt;a href=&quot;https://store.steampowered.com/app/2132850/Rabbit_and_Steel/&quot;&gt;Rabbit and Steel&lt;/a&gt;. I traversed through GitHub to find a GameMaker modding toolkit that wasn’t bad, until I stumbled upon &lt;a href=&quot;https://github.com/AurieFramework/YYToolkit&quot;&gt;YYToolkit&lt;/a&gt;. YYToolkit is the only good GameMaker modding tool I’ve seen so far, because they actually understand modifying a singular data file is not a feasible way to make mods. I made some mods for it, but eventually abandoned it in favor of &lt;a href=&quot;https://github.com/NotNite/RNSReloaded&quot;&gt;writing my own&lt;/a&gt; using &lt;a href=&quot;https://reloaded-project.github.io/Reloaded-II/&quot;&gt;Reloaded II&lt;/a&gt;, because I didn’t like C++. Nowadays, that’s maintained by some other people, who I have endless thanks for keeping it going.&lt;/p&gt;
&lt;p&gt;I also &lt;a href=&quot;https://github.com/Sewer56/Reloaded.Imgui.Hook/pull/8&quot;&gt;found a bug in a Reloaded library&lt;/a&gt; and was continually amazed by how fast Sewer56 merges pull requests.&lt;/p&gt;
&lt;h2 id=&quot;june&quot;&gt;June&lt;/h2&gt;
&lt;p&gt;June was a not-so-busy month for me due to the release of FINAL FANTASY XIV: Dawntrail. To prepare for a lack of plugins, I made &lt;a href=&quot;https://github.com/NotNite/TemporalStasis.Chronofoil&quot;&gt;a packet archiver with the Chronofoil format using Temporal Stasis&lt;/a&gt;. That’s some absurdly long text for a link.&lt;/p&gt;
&lt;p&gt;Most of this time was spent playing FFXIV or datamining it, so there’s not much to talk about here. The two-day maintenance really threw me off time wise…&lt;/p&gt;
&lt;p&gt;The one thing of note I did work on is rewriting &lt;a href=&quot;https://github.com/NotNite/Linkpearl&quot;&gt;my plugin Linkpearl&lt;/a&gt; to be nicer. A pull request I merged for a rewrite really messed up the code quality, so I did it but better.&lt;/p&gt;
&lt;h2 id=&quot;july&quot;&gt;July&lt;/h2&gt;
&lt;p&gt;I spent most of the beginning of July updating FFXIV plugins. I also switched to &lt;a href=&quot;https://binary.ninja/&quot;&gt;Binary Ninja&lt;/a&gt; as my decompiler, and &lt;a href=&quot;https://github.com/NotNite/searchlight&quot;&gt;wrote a signature scanner&lt;/a&gt; and &lt;a href=&quot;https://github.com/aers/FFXIVClientStructs/pull/1008&quot;&gt;added support to our FFXIV reverse engineering database&lt;/a&gt; for it.&lt;/p&gt;
&lt;p&gt;I got a new server, which I named “electrope” after the new FFXIV expansion, and set it up using Rocky Linux and Docker. I go into this setup a bit more &lt;a href=&quot;https://notnite.com/blog/home-server&quot;&gt;in my home server blog post&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;On the 20th, I made a FFXIV damage overlay using FFLogs’ DPS parser. This upset FFLogs, understandably, and I ended up shutting the project down soon later. It was really fun to work on, though, and I’m thankful that people liked it while it lasted.&lt;/p&gt;
&lt;p&gt;I closed off the month by working on &lt;a href=&quot;https://github.com/ValveSoftware/GameNetworkingSockets&quot;&gt;GameNetworkingSockets&lt;/a&gt; bindings for C#, which I never finished. Whoops!&lt;/p&gt;
&lt;h2 id=&quot;august&quot;&gt;August&lt;/h2&gt;
&lt;p&gt;In August, I &lt;a href=&quot;https://notnite.com/blog/ps3&quot;&gt;wrote another blog post about the PS3&lt;/a&gt;. This is my favorite blog post that I wrote this year - if you haven’t seen it yet, go check it out! I ended up making some friends in the PS3 scene from it, which was nice.&lt;/p&gt;
&lt;p&gt;Beyond that, not much happened, as I was more focused on my girlfriend visiting me this month. We did end up going to a King Gizzard &amp;#x26; The Lizard Wizard concert, though!&lt;/p&gt;
&lt;h2 id=&quot;september&quot;&gt;September&lt;/h2&gt;
&lt;p&gt;The 1st was my birthday! I turned 18, which was weird to think about, given I’ve known a lot of my friends since I was a wee little teen. Most people know me as the youngest person in the relevant community, and so I gave them a bit of a heart attack by saying that I can [insert boring adult thing here]. My friends celebrated my birthday by playing TTT in Garry’s Mod.&lt;/p&gt;
&lt;p&gt;On the 4th, I &lt;a href=&quot;https://github.com/NotNite/Alpha/commit/a167443ee5d6b3e532eb34fee06db2680ab1a7f6&quot;&gt;published my rewrite of Alpha&lt;/a&gt;, which was sitting unstaged since last Thanksgiving. I lost motivation to work on it, but I ended up finding it, which I’m happy about.&lt;/p&gt;
&lt;p&gt;On the 18th, I started work on a personal desktop wallpaper program tool in the style of Wallpaper Engine, but never finished it. It had a lot of fun features like a media player, todo list viewer, calendar, and system usage graphs.&lt;/p&gt;
&lt;p&gt;I got into a bit of a Satisfactory kick this month (with its 1.0 release), and &lt;a href=&quot;https://github.com/NotNite/Statisfactory&quot;&gt;made a mod for it to send statistics to Grafana&lt;/a&gt;. Someone ended up making a “better” version of this plugin, but it was still fun to work on, and I got to farm like 10 Reddit upvotes. While working on it, I had to debug an issue with the Satisfactory modding toolchain, which luckily involved C# (my favorite language). Did I say I hate writing C++ yet?&lt;/p&gt;
&lt;p&gt;I reinstalled Windows this month, after several years of using the same Windows install, and ended up getting new drives. There’s a lot more I would’ve added to this post if I had all the source code to recount dates, but unfortunately they’re on hard drives that aren’t connected anymore, so it’s a bit of a pain.&lt;/p&gt;
&lt;p&gt;On the 23rd, I redesigned my website (you’re probably reading this post on that design), and &lt;a href=&quot;https://notnite.com/blog/website-redesign&quot;&gt;wrote a blog post&lt;/a&gt; as tradition.&lt;/p&gt;
&lt;h2 id=&quot;october&quot;&gt;October&lt;/h2&gt;
&lt;p&gt;October was my most active month by far. I’m not really sure why, but it sure was fun!&lt;/p&gt;
&lt;p&gt;There’s so much action here that I won’t even bother dating it. I released &lt;a href=&quot;https://moonlight-mod.github.io/blog/moonlight-api-v2/&quot;&gt;not one&lt;/a&gt; but &lt;a href=&quot;https://moonlight-mod.github.io/blog/moonlight-1-2/&quot;&gt;two&lt;/a&gt; major releases for my Discord client mod &lt;a href=&quot;https://moonlight-mod.github.io/&quot;&gt;moonlight&lt;/a&gt;. This was a very nice speedup from the lack of updates over the month, and I now use moonlight on my main Discord client to force myself to fix things if they break.&lt;/p&gt;
&lt;p&gt;I also &lt;a href=&quot;https://github.com/NotNite/GDWeave&quot;&gt;made GDWeave&lt;/a&gt; - a modloader for Godot games - for WEBFISHING. I spent a lot of time in that community making &lt;a href=&quot;https://github.com/NotNite/WebfishingPlus&quot;&gt;mods&lt;/a&gt; and &lt;a href=&quot;https://github.com/NotNite/webfishing-save-editor&quot;&gt;save editors&lt;/a&gt; and &lt;a href=&quot;https://github.com/NotNite/manifestation&quot;&gt;mod packagers&lt;/a&gt; and &lt;a href=&quot;https://github.com/NotNite/webfishing-mod-wiki&quot;&gt;wikis&lt;/a&gt; and such. The game now sits on &lt;a href=&quot;https://thunderstore.io/c/webfishing/&quot;&gt;Thunderstore&lt;/a&gt;, where the moderators were very kind to me this time around.&lt;/p&gt;
&lt;p&gt;I also found remote code execution in WEBFISHING (with the help of my friend &lt;a href=&quot;https://github.com/katietheqt&quot;&gt;Katie&lt;/a&gt;), which I had a blog post and everything written for, but scrapped it due to the developer’s request. I accidentally published a draft of this through my RSS feed, so if you got to read it, I hope you liked it (lol).&lt;/p&gt;
&lt;h2 id=&quot;november&quot;&gt;November&lt;/h2&gt;
&lt;p&gt;Continuing the WEBFISHING trend, I &lt;a href=&quot;https://github.com/ebkr/r2modmanPlus/pull/1514&quot;&gt;added support for GDWeave into r2modman&lt;/a&gt;. This was really fun to work on, and I have a lot of thanks for the developers of r2modman/Thunderstore for being so patient and helpful with me. Working on r2modman required me to touch ancient Node.js versions, so now I use &lt;a href=&quot;https://volta.sh/&quot;&gt;Volta&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I wrapped up my hyperfocus on moonlight with &lt;a href=&quot;https://github.com/moonlight-mod/robojules&quot;&gt;RoboJules&lt;/a&gt;, a tool to review pull requests for moonlight extensions.&lt;/p&gt;
&lt;p&gt;On the 16th, I &lt;a href=&quot;https://github.com/NotNite/notnote&quot;&gt;made a pastebin&lt;/a&gt; but never did anything with it because I realized hosting pastebins on the internet without any access controls is kind of a bad idea.&lt;/p&gt;
&lt;p&gt;On the 19th, I made a (really barebones) FFXIV server emulator in C#. This never went anywhere because of concerns matching retail, but it had a lot of cool features - including supporting multiple versions in one branch! I’d love to continue work on it someday, but server emulators are hard to work on and I don’t really want to work on it in general given it’s a bad idea to have stuff like that for the retail game client.&lt;/p&gt;
&lt;p&gt;Towards the end of the month, I began work on reviving &lt;a href=&quot;https://www.twitch.tv/twitchplaysxiv&quot;&gt;Twitch Plays FFXIV&lt;/a&gt; (that was me by the way), but I haven’t set it up fully yet. Maybe soon!&lt;/p&gt;
&lt;p&gt;To close the month off, I made &lt;a href=&quot;https://github.com/NotNite/rogue&quot;&gt;rogue&lt;/a&gt;, helper scripts for FFXIV in Binary Ninja that were much better than the original ClientStructs PR I made earlier this year.&lt;/p&gt;
&lt;h2 id=&quot;december&quot;&gt;December&lt;/h2&gt;
&lt;p&gt;I got a new home server, and expectedly, &lt;a href=&quot;https://notnite.com/blog/home-server&quot;&gt;wrote a blog post about it&lt;/a&gt;. This project took up several weeks of my time, so it’s a good explanation of what I did during December.&lt;/p&gt;
&lt;p&gt;I also spent Christmas at home with family, and got new monitors for my main computer. My only pet project this month has been &lt;a href=&quot;https://github.com/NotNite/SteamTimelines&quot;&gt;Steam Timelines support for FFXIV&lt;/a&gt;, which is an excellent break from the chaos of October and November.&lt;/p&gt;
&lt;p&gt;I spent the last week of the year getting into Old School RuneScape, for some reason. I completed every free to play quest!&lt;/p&gt;
&lt;h2 id=&quot;for-2025&quot;&gt;For 2025&lt;/h2&gt;
&lt;p&gt;I want to do a lot more in 2025, and hopefully build more software that makes people happy. In 2023, my biggest project of the year was &lt;a href=&quot;https://github.com/SlopCrew/SlopCrew&quot;&gt;Slop Crew&lt;/a&gt;, and this year’s project was &lt;a href=&quot;https://github.com/NotNite/GDWeave&quot;&gt;GDWeave&lt;/a&gt;. Maybe I can do something better for next year? In any case, this year was definitely worse than last year, but not by much.&lt;/p&gt;
&lt;p&gt;People tell me my work ethic is a bit unusual, and I never really got it until after writing this blog post. Reflecting on the things I’ve done, it kind of demonstrates that I never really sit down and focus on something (haha ADHD go brrr), I just bounce between what makes me happy. Burnout doesn’t really seem to exist for me as much as breaks - after all, it’s been about three years of building software actively and I still love it. This’ll probably change when I get a job, because right now this is all hobby stuff.&lt;/p&gt;
&lt;p&gt;Thanks to all my friends and family for supporting me this year and giving me so many opportunities. Thanks to everyone who donated to me this year &lt;a href=&quot;https://notnite.com/givememoney&quot;&gt;on GitHub Sponsors&lt;/a&gt;. And, most of all, thank you for reading and caring about what I’ve been up to this year! I hope it’s been an interesting read seeing all I’ve done.&lt;/p&gt;</content:encoded></item><item><title>Building a new home server</title><link>https://notnite.com/blog/home-server</link><guid isPermaLink="true">https://notnite.com/blog/home-server</guid><description>See you next year for when I get tired of this setup</description><pubDate>Sat, 21 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;A few days ago, I got an email from my server running Proxmox:&lt;/p&gt;
&lt;blockquote class=&quot;bluesky-embed&quot; data-bluesky-uri=&quot;at://did:plc:ra3gxl2udc22odfbvcfslcn3/app.bsky.feed.post/3lct2kfun6s27&quot; data-bluesky-cid=&quot;bafyreiboxltyeyp6m7myotr7cosf3m6f7hvjwo5ddfbbnuitkspveobjem&quot;&gt;&lt;p lang=&quot;en&quot;&gt;well that&apos;s not good&lt;br&gt;&lt;br&gt;&lt;a href=&quot;https://bsky.app/profile/did:plc:ra3gxl2udc22odfbvcfslcn3/post/3lct2kfun6s27?ref_src=embed&quot;&gt;[image or embed]&lt;/a&gt;&lt;/p&gt;— NotNite (&lt;a href=&quot;https://bsky.app/profile/did:plc:ra3gxl2udc22odfbvcfslcn3?ref_src=embed&quot;&gt;@notnite.com&lt;/a&gt;) &lt;a href=&quot;https://bsky.app/profile/did:plc:ra3gxl2udc22odfbvcfslcn3/post/3lct2kfun6s27?ref_src=embed&quot;&gt;Dec 8, 2024 at 3:31 PM&lt;/a&gt;&lt;/blockquote&gt;
&lt;p&gt;One of the drives was showing signs of failure. This was bad for me, as this drive was part of a RAID group that I mistakenly set up to be RAID 0 several years ago. I realized it was RAID 0 &lt;em&gt;after&lt;/em&gt; I got this email. Whoops!&lt;/p&gt;
&lt;p&gt;I was wanting to build a new server + NAS combination for a while, so this seemed like a good opportunity to do it, but with some extra time pressure spice on top of it. Let’s talk about it!&lt;/p&gt;
&lt;h2 id=&quot;the-notnet-server-saga&quot;&gt;The NotNet server saga&lt;/h2&gt;
&lt;p&gt;The first server I ever owned was a hand-me-down from my dad’s workplace. It was an old Dell OptiPlex, and it could run a web server and a really limited Minecraft world before running out of resources. Later that year, I ended up buying a Hetzner dedicated server, which would host most of my projects. I strived for something more local, though - some place that I could experiment with, and more importantly get better ping to. Enter one Christmas day, where I was able to build my own home server. This is the one that this website was hosted on, up until today!&lt;/p&gt;
&lt;p&gt;Eventually, I started using NixOS instead of Debian for the LXC containers in Proxmox, and I quickly learned that it was a big mistake. This caused me lots of grief with bugs in the integration and a general feeling of instability - especially when there was practically no way to test service changes locally. I also encountered issues where my Mastodon instance &lt;em&gt;couldn’t update to patch a CVE&lt;/em&gt; because nixpkgs didn’t support that version of Yarn lockfile at the time.&lt;/p&gt;
&lt;p&gt;I ended up buying a second Hetzner dedicated server, installed Rocky Linux on it, and started using Docker for everything. Instead of using Kubernetes or Docker Swarm, I just use some Docker Compose files with a bit of shell scripting to glue it all together + do backups every day. I don’t need that complexity, and this is very easy to test locally. I’m still in the progress of migrating everything off of the first server 🥲.&lt;/p&gt;
&lt;p&gt;I chose Docker over Podman because Docker Compose is battle tested and podman-compose seemed iffy. I would prefer the ability to make rootless containers, but I really wanted to be able to use YAML to declare stuff instead of whatever the hell a Quadlet is.&lt;/p&gt;
&lt;p&gt;The layout of the files is like this:&lt;/p&gt;
&lt;pre class=&quot;language-plaintext&quot; data-language=&quot;plaintext&quot;&gt;&lt;code class=&quot;language-plaintext&quot;&gt;composes/
  (stack name)/
    compose.yaml
    (optional config files, like a Caddyfile)
scripts/
  (misc shell scripts for server administration)
  update.sh
  backup.sh
volume-exclude.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Every day, a systemd timer runs &lt;code&gt;backup.sh&lt;/code&gt;, which backs up the folders in the &lt;code&gt;volumes&lt;/code&gt; directory using &lt;a href=&quot;https://www.borgbackup.org/&quot;&gt;BorgBackup&lt;/a&gt;. Every 30 minutes, another systemd timer runs &lt;code&gt;update.sh&lt;/code&gt;, which pulls the latest changes from a Git repository and pulls/rebuilds the latest versions of every container.&lt;/p&gt;
&lt;p&gt;This system has a lot of flaws, but it’s the one that works best for me and my use cases. While it does make running services for other people a lot more inconvenient, I wanted to stray away from doing that anyway, and keep my services to only myself. Things that I have to maintain that don’t impact me stress me out, and I don’t need that in my life.&lt;/p&gt;
&lt;p&gt;I ended up taking that repository, modified the paths/names on it, and used it for the new server. Now, let’s get to setting it up!&lt;/p&gt;
&lt;h2 id=&quot;os--storage&quot;&gt;OS &amp;#x26; storage&lt;/h2&gt;
&lt;p&gt;The machine is an old gaming PC of mine, with an i7-8700 with 32 GB of RAM. It also has an RTX 3060 in it, because it used to be a server for AI workloads, but I no longer have interest in the AI sector of technology in the slightest after seeing how terrible it has become. (That’s a blog post for another day.)&lt;/p&gt;
&lt;p&gt;As with my latest server, I chose Rocky Linux. I have increasingly warmed up to RHEL-land and &lt;code&gt;dnf&lt;/code&gt; (especially after using Fedora on my laptop), and a friend suggested it to me, so I thought why not. I’m going with the hostname “cabbit” - a portmanteau of “cat” and “rabbit” - because I was thinking about cabbits while flashing the installation ISO.&lt;/p&gt;
&lt;p&gt;The rootfs will be a 2 TB SSD from a previous project. After installation, I installed Docker and set up the infrastructure user. There was a problem, though - I noticed that the installer only allocated &lt;em&gt;70 GB&lt;/em&gt; for the rootfs, while the rest of the almost 2 TB went to the /home partition! I ended up &lt;a href=&quot;https://gist.github.com/troyfontaine/87091bd6a5c68f45dd62ced3d12bc377&quot;&gt;finding a GitHub Gist&lt;/a&gt; on how to delete the home partition and resize the root, so I did that (with some help from the comments when I encountered an error message).&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Output of hyfetch in Windows Terminal, with a custom cabbit logo&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;1113&quot; height=&quot;656&quot; src=&quot;https://notnite.com/_astro/hyfetch.BkkEfaX8_Z2i0uNN.webp&quot;&gt;&lt;/p&gt;
&lt;p&gt;After all that, I just had to wait for the drives to arrive. We’re going with 4x14 TB hard drives - WD Reds - in a ZFS cluster. I picked ZFS because it seemed the most stable and I had very little experience with it, which I wanted to sharpen my skills with. I picked one up from the local Micro Center and ordered the other three online, so the chances of having multiple from the same batch was less.&lt;/p&gt;
&lt;p&gt;After Newegg delayed my drive through the weekend, I got the drives installed and set up a ZFS pool &amp;#x26; filesystem with raidz2. I chose the name “poolian” because it’s funny.&lt;/p&gt;
&lt;p&gt;I went with the following structure on disk:&lt;/p&gt;
&lt;pre class=&quot;language-plaintext&quot; data-language=&quot;plaintext&quot;&gt;&lt;code class=&quot;language-plaintext&quot;&gt;volumes/
  (various folders for Docker bind mounting)
media/
  (shared folder for family files)
samba/
  (per-user samba shares)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I set up a Samba share for me and my dad, and then started working on the first service to be deployed (a Jellyfin instance).&lt;/p&gt;
&lt;h2 id=&quot;deploying-jellyfin-over-tailscale&quot;&gt;Deploying Jellyfin over Tailscale&lt;/h2&gt;
&lt;p&gt;I love &lt;a href=&quot;https://tailscale.com/&quot;&gt;Tailscale&lt;/a&gt; a lot, but never really used a lot of its potential. I wanted to try serving some internal services in my home (Jellyfin, Home Assistant, etc.) through it. I ended up finding &lt;a href=&quot;https://github.com/tailscale/caddy-tailscale&quot;&gt;caddy-tailscale&lt;/a&gt;, and it seems like a perfect fit - I can create ephemeral nodes that proxy to other services!&lt;/p&gt;
&lt;p&gt;My composes follow the prefix of &lt;code&gt;type-service&lt;/code&gt;, so &lt;code&gt;web-caddy&lt;/code&gt; for websites. For these internal things, I adopted the &lt;code&gt;internal&lt;/code&gt; type, so &lt;code&gt;internal-caddy&lt;/code&gt; and &lt;code&gt;internal-jellyfin&lt;/code&gt; and so on. After stealing a Dockerfile from a GitHub issue, configuring the Caddyfile was easy:&lt;/p&gt;
&lt;pre class=&quot;language-plaintext&quot; data-language=&quot;plaintext&quot;&gt;&lt;code class=&quot;language-plaintext&quot;&gt;{
  email redacted
  tailscale {
    ephemeral
  }
}

:80 {
  bind tailscale/jellyfin
  reverse_proxy jellyfin:8096
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then, I just had to make an &lt;code&gt;internal-caddy&lt;/code&gt; Docker network and put the Jellyfin container into it. I also exposed the port through Docker to the LAN, so devices like smart TVs don’t need a Tailscale connection.&lt;/p&gt;
&lt;p&gt;As said before, this machine also has a GPU in it, so I was able to connect it to Jellyfin for hardware encoding. I already had the NVIDIA drivers installed, so I just had to install an extra package and run a command to set up Docker support, then edit the compose.yaml to use it. &lt;a href=&quot;https://jellyfin.org/docs/general/administration/hardware-acceleration/nvidia/&quot;&gt;There’s a full tutorial on Jellyfin’s documentation site&lt;/a&gt; which does a better job at explaining it.&lt;/p&gt;
&lt;h2 id=&quot;moving-the-rest&quot;&gt;Moving the rest&lt;/h2&gt;
&lt;p&gt;I don’t want to have any downtime, so I copied my NGINX configs into Caddy 1:1 and copied the website directories over. The idea was that I’ll just change the port in my firewall, start Caddy, and it’ll reverse proxy to the old existing services until I can Dockerize them. I was very pleased with how easy and readable the Caddyfile config for this was!&lt;/p&gt;
&lt;p&gt;I changed the destination address in my firewall, started the service, and… it just worked? Nice! Literally &lt;em&gt;everything&lt;/em&gt; worked first try, which I’m very happy with.&lt;/p&gt;
&lt;p&gt;I first moved Forgejo and the LDAP server that it uses. Moving the LDAP server was fine, but the export from the &lt;code&gt;gitea dump&lt;/code&gt; command didn’t actually line up with either the paths on disk or in the Docker container, which was very confusing. I spun up a test instance of the Docker container and compared the filesystem to figure out where to put everything, and had to do some config file surgery for the data directory and missing fields that were only in the Docker container.&lt;/p&gt;
&lt;p&gt;There’s a lot of stuff that wasn’t properly declared through Nix on the old server - one off services that don’t need to be running for long, or things that are more personal projects. Moving these was mostly easy, because not being bound by Nix makes it easy to do things the non-Nix way. I can just install and run Rust instead of needing to pop open a Nix shell or modify the environment. It’s simpler for quick hacks, and that’s the point.&lt;/p&gt;
&lt;p&gt;Could I have declared them? Yes - at a cost. The services need to be reachable to build (= in a repository that’s either public or has access configured). It required me to write NixOS modules for everything. This just isn’t worth it for a personal Discord bot or an &lt;a href=&quot;https://eightyeightthirty.one/&quot;&gt;eightyeightthirty.one&lt;/a&gt; scraper node - these aren’t important enough with my time for me to Nix-ify it.&lt;/p&gt;
&lt;p&gt;That’s my real gripe with Nix - it works when it works, but breaking out of that declarative box is so hard, especially with how running executables works with the interpreter. The ecosystem is designed intentionally to force you into doing everything the Nix way, for better and for worse, and the worse parts bit me far more than the better parts. I have a lot of respect for people who have the time to configure it, but I don’t want to be doing that for &lt;em&gt;everything&lt;/em&gt;.&lt;/p&gt;
&lt;h2 id=&quot;the-results&quot;&gt;The results&lt;/h2&gt;
&lt;p&gt;Things… just work! I started drafting this blog post shortly before I built the machine, and it’s been around a week since as I type this paragraph. ZFS is eating a lot of memory, but it’s not a huge concern. There’s a few services I have yet to migrate which I’ll probably kill off:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;hast =), because running a paste server that anyone can use is a liability&lt;/li&gt;
&lt;li&gt;The Lounge, because I use Quassel now for IRC&lt;/li&gt;
&lt;li&gt;Some misc game servers that haven’t seen users in months&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I’ll be taking a backup of everything that I care about and shutting it off for good sometime soon. Overall, I’m really happy with this move - server maintenance is a lot less stressful for me now. The storage will come in handy for my family, and I’ll probably be lending some storage space to some close friends with &lt;a href=&quot;https://borgbackup.readthedocs.io/en/stable/deployment/hosting-repositories.html&quot;&gt;BorgBackup&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Onto worrying about migrating my Hetzner machines…&lt;/p&gt;</content:encoded></item><item><title>Tips on making a good website with Astro</title><link>https://notnite.com/blog/astro-tips</link><guid isPermaLink="true">https://notnite.com/blog/astro-tips</guid><description>Go make a website! Do it now!</description><pubDate>Wed, 25 Sep 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://astro.build/&quot;&gt;Astro&lt;/a&gt; is my preferred framework of choice for making websites. Here’s a few tips I learned, particularly so I can link this to my friends when they ask me how to make a website in the future. These are ranked from most important to least important.&lt;/p&gt;
&lt;h2 id=&quot;follow-the-astro-blog-tutorial&quot;&gt;Follow the Astro blog tutorial&lt;/h2&gt;
&lt;p&gt;Astro has a very good &lt;a href=&quot;https://docs.astro.build/en/tutorial/0-introduction/&quot;&gt;blog tutorial&lt;/a&gt; that teaches you the basics. Go try it!&lt;/p&gt;
&lt;p&gt;Skip their suggestion for Netlify and &lt;a href=&quot;#pick-a-good-hosting-provider&quot;&gt;pick a good hosting provider&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;pick-a-good-hosting-provider&quot;&gt;Pick a good hosting provider&lt;/h2&gt;
&lt;p&gt;Don’t get scammed by “serverless” providers that have hidden fees. Use something simple. My suggestions are either host your static files on a VPS or use &lt;a href=&quot;https://docs.astro.build/en/guides/deploy/github/&quot;&gt;GitHub Pages&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;use-a-content-collection&quot;&gt;Use a content collection&lt;/h2&gt;
&lt;p&gt;For blog posts, &lt;code&gt;Astro.glob&lt;/code&gt; &lt;em&gt;works&lt;/em&gt;, but it doesn’t let you do much with it. I suggest setting up a proper &lt;a href=&quot;https://docs.astro.build/en/guides/content-collections/&quot;&gt;content collection&lt;/a&gt;. Here’s mine:&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot; data-language=&quot;ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// src/content/config.ts&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; defineCollection&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; z &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;astro:content&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; blog &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;defineCollection&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  type&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;content&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token function-variable function&quot;&gt;schema&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; image &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt;
    z&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;object&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      title&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; z&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      description&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; z&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      date&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; z&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;date&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      cover&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;optional&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; collections &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  blog
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;structure-your-layouts-well&quot;&gt;Structure your layouts well&lt;/h2&gt;
&lt;p&gt;Personally, I have a main &lt;code&gt;Layout.astro&lt;/code&gt; with the main site layout, styles and all, with a slot in &lt;code&gt;&amp;#x3C;main&gt;&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;You can make one for rendering pages written in Markdown:&lt;/p&gt;
&lt;pre class=&quot;language-astro&quot; data-language=&quot;astro&quot;&gt;&lt;code class=&quot;language-astro&quot;&gt;&lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; Layout &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;./Layout.astro&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;

&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Layout&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token spread&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;Astro&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;props&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;frontmatter&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;#x3C;&lt;/span&gt;slot&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;#x3C;/&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Layout&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then add &lt;code&gt;layout: &quot;../layouts/Markdown.astro&quot;&lt;/code&gt; to the frontmatter.&lt;/p&gt;
&lt;h2 id=&quot;set-up-a-rss-feed-with-containers&quot;&gt;Set up a RSS feed with containers&lt;/h2&gt;
&lt;p&gt;The experimental &lt;a href=&quot;https://docs.astro.build/en/reference/container-reference/&quot;&gt;Astro Container API&lt;/a&gt; allows you to render your Markdown into HTML which can be embedded into your RSS feed. This significantly improves the experience in feed readers.&lt;/p&gt;
&lt;p&gt;First, &lt;a href=&quot;https://docs.astro.build/en/guides/rss/&quot;&gt;set up RSS&lt;/a&gt;. Make sure to add the link for RSS auto discovery. Then, &lt;a href=&quot;https://gist.github.com/NotNite/dedfa28a26f8b3ceb482c230d90ad2c6&quot;&gt;use the code in my gist&lt;/a&gt; to render the RSS feed.&lt;/p&gt;
&lt;h2 id=&quot;setup-mdx&quot;&gt;Setup MDX&lt;/h2&gt;
&lt;p&gt;With MDX, you can use your components in your Markdown files. I have one for a video player and a little info box for quotes. See &lt;a href=&quot;https://docs.astro.build/en/guides/integrations-guide/mdx/&quot;&gt;the entry on the Astro docs&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;If you plan to lint this, see &lt;a href=&quot;#set-up-formatters&quot;&gt;my comments about MDX linting&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;create-an-88x31&quot;&gt;Create an 88x31&lt;/h2&gt;
&lt;p&gt;88x31s are small badges, usually linking to other websites, that usually promote some sort of person/project/program/etc. &lt;a href=&quot;https://notnite.com/&quot;&gt;My website has a ton of them!&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;My friend Ari has &lt;a href=&quot;https://adryd.com/pages/88x31-notes/&quot;&gt;some good notes&lt;/a&gt; on how to make a good one. You can use any image editor you’d like as long as you can set the size. (Why a specific size? It’s Geocities’ fault!)&lt;/p&gt;
&lt;p&gt;Ask your friends to add your 88x31, and if you can add theirs. When adding 88x31s, &lt;strong&gt;avoid &lt;a href=&quot;https://docs.astro.build/en/guides/images/&quot;&gt;letting Astro process them&lt;/a&gt;&lt;/strong&gt;, as it can ruin the quality. Put them in your &lt;code&gt;public/&lt;/code&gt; directory raw (I do &lt;code&gt;public/buttons/&lt;/code&gt;).&lt;/p&gt;
&lt;h2 id=&quot;typescript-import-aliases&quot;&gt;TypeScript import aliases&lt;/h2&gt;
&lt;p&gt;Turn this:&lt;/p&gt;
&lt;pre class=&quot;language-js&quot; data-language=&quot;js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; MyComponent &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;../../components/MyComponent.astro&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;into:&lt;/p&gt;
&lt;pre class=&quot;language-js&quot; data-language=&quot;js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; MyComponent &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;@components/MyComponent.astro&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;See &lt;a href=&quot;https://docs.astro.build/en/guides/typescript/#import-aliases&quot;&gt;the entry on the Astro docs&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;set-up-formatters&quot;&gt;Set up formatters&lt;/h2&gt;
&lt;p&gt;Personally:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://prettier.io/&quot;&gt;Prettier&lt;/a&gt; for formatting
&lt;ul&gt;
&lt;li&gt;Install &lt;a href=&quot;https://github.com/withastro/prettier-plugin-astro&quot;&gt;the Astro plugin&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://eslint.org/&quot;&gt;ESLint&lt;/a&gt; for linting
&lt;ul&gt;
&lt;li&gt;Install &lt;a href=&quot;https://www.npmjs.com/package/eslint-plugin-astro&quot;&gt;the Astro plugin&lt;/a&gt; and &lt;a href=&quot;https://github.com/prettier/eslint-config-prettier&quot;&gt;the Prettier config&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/DavidAnson/markdownlint&quot;&gt;markdownlint&lt;/a&gt; for Markdown&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I would like to lint &lt;a href=&quot;https://github.com/mdx-js/mdx&quot;&gt;MDX&lt;/a&gt;, but &lt;a href=&quot;https://github.com/remarkjs/remark&quot;&gt;remark&lt;/a&gt; just seems very bad (especially with dependency management and linting codeblocks), so I just write code with my editor set to vanilla Markdown and write the MDX imports by hand. Suggestions welcome!&lt;/p&gt;</content:encoded></item><item><title>Redesigning my website</title><link>https://notnite.com/blog/website-redesign</link><guid isPermaLink="true">https://notnite.com/blog/website-redesign</guid><description>...with the power of Astro and friendship</description><pubDate>Mon, 23 Sep 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Today I redesigned my website. If you’re reading this, it’s probably that new website! Let’s talk about what changed and how I did it. (This one will be a short one.)&lt;/p&gt;
&lt;h2 id=&quot;motivations-for-redesigning&quot;&gt;Motivations for redesigning&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Add better accessibility features
&lt;ul&gt;
&lt;li&gt;Many people have complained about the colors, font, and layout. Let them change all of them&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Fix the janky CSS the current site relies on
&lt;ul&gt;
&lt;li&gt;Fun fact: On the old site, you could scroll to extra whitespace on the side on Firefox Mobile, and only Firefox Mobile for some reason&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Clean up the messy Markdown plugins
&lt;ul&gt;
&lt;li&gt;I use a few to fix some quirks in the rendered Markdown output, but I want to not rely on it anymore&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Having fun!&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;picking-a-framework&quot;&gt;Picking a framework&lt;/h2&gt;
&lt;p&gt;This site, along with my “old” site, is &lt;a href=&quot;https://astro.build/&quot;&gt;Astro&lt;/a&gt;. I like Astro for a number of reasons:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Simple component model with slots&lt;/li&gt;
&lt;li&gt;Static site generator by default&lt;/li&gt;
&lt;li&gt;Mix in other JS frameworks (e.g. &lt;a href=&quot;https://svelte.dev/&quot;&gt;Svelte&lt;/a&gt;, which I did!)&lt;/li&gt;
&lt;li&gt;You can write &lt;a href=&quot;https://www.typescriptlang.org/&quot;&gt;TypeScript&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I ended up making a new project but I copied over 90% of the files, so I’m not really sure if this can be considered a “new” site. Was it a new site when I switched from &lt;a href=&quot;https://www.getzola.org/&quot;&gt;Zola&lt;/a&gt;, taking the layout with it, even though it was an entirely new backend generator?&lt;/p&gt;
&lt;p&gt;As I said above, I enabled the Svelte integration for some dynamic components (which we’ll talk about later).&lt;/p&gt;
&lt;h2 id=&quot;redoing-the-styling&quot;&gt;Redoing the styling&lt;/h2&gt;
&lt;p&gt;My old CSS was a mess with hardcoded sizes and &lt;code&gt;calc&lt;/code&gt; everywhere. To make it more simple, I’ve ditched a majority of my headaches with the main container with some sizing fixes, along with using CSS variables to calculate the sizes that I need to hardcode. I used &lt;a href=&quot;https://sass-lang.com/&quot;&gt;Sass&lt;/a&gt; to automatically generate some of the annoying bits.&lt;/p&gt;
&lt;h2 id=&quot;new-features&quot;&gt;New features&lt;/h2&gt;
&lt;p&gt;At the top of your screen is a nice path showing what file you’re in. You can click on each folder (or the root “notnite” node) to get to where you want to go.&lt;/p&gt;
&lt;p&gt;The real star of the show, though, is the new theme button. Click on it to open a panel. This panel stores configuration in &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage&quot;&gt;localStorage&lt;/a&gt;, and can change CSS classes on the fly. It’s the only part of the website that uses Svelte so far.&lt;/p&gt;
&lt;p&gt;This was the hardest part of the entire redesign, and the main focus of it.&lt;/p&gt;
&lt;h2 id=&quot;applying-themes-without-looking-bad&quot;&gt;Applying themes without looking bad&lt;/h2&gt;
&lt;p&gt;The biggest issue was that the themes need to be applied before the page loads, or you’ll get a flash of the wrong color, which can be very disorienting. To solve this, I wrote some JavaScript at the root of the body tag marked with &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#defer&quot;&gt;defer&lt;/a&gt;. It sets up the defaults and exposes some functions in the &lt;code&gt;window&lt;/code&gt; object so the Svelte component can also set the theme.&lt;/p&gt;
&lt;p&gt;These exposed functions are hacky, but it works. This even means you can use them! Try running this in DevTools:&lt;/p&gt;
&lt;pre class=&quot;language-js&quot; data-language=&quot;js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;window&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;__notnite_theme_set&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;color&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;nitelight&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I also manually edit the source URL of the &lt;a href=&quot;https://giscus.app/&quot;&gt;Giscus&lt;/a&gt; frame when the color changes, so that it stays in sync with the theme of the site. Giscus, if you are unaware, is the comment section at the bottom of this blog.&lt;/p&gt;
&lt;h2 id=&quot;using-the-theme-in-css&quot;&gt;Using the theme in CSS&lt;/h2&gt;
&lt;p&gt;With the classes at the root of the body, it’s pretty easy to apply them to other elements:&lt;/p&gt;
&lt;pre class=&quot;language-scss&quot; data-language=&quot;scss&quot;&gt;&lt;code class=&quot;language-scss&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;.fontInconsolata &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;--font&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Inconsolata&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; monospace&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token selector&quot;&gt;.fontInter &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;--font&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Inter&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; sans-serif&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token selector&quot;&gt;.positionRight main &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;text-align&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; right&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token property&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$sizes&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
  &lt;span class=&quot;token string&quot;&gt;&quot;Normal&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 35rem&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token string&quot;&gt;&quot;More&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 50rem&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token string&quot;&gt;&quot;All&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 100%
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;@each&lt;/span&gt; &lt;span class=&quot;token selector&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$size&lt;/span&gt;, &lt;span class=&quot;token variable&quot;&gt;$value&lt;/span&gt; in &lt;span class=&quot;token variable&quot;&gt;$sizes&lt;/span&gt; &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// --size is needed on multiple components for proper sizing&lt;/span&gt;
  &lt;span class=&quot;token selector&quot;&gt;.size&lt;span class=&quot;token variable&quot;&gt;#{$size}&lt;/span&gt; main,
  .size&lt;span class=&quot;token variable&quot;&gt;#{$size}&lt;/span&gt; &gt; .container,
  .size&lt;span class=&quot;token variable&quot;&gt;#{$size}&lt;/span&gt; &gt; pre,
  .size&lt;span class=&quot;token variable&quot;&gt;#{$size}&lt;/span&gt; &gt; img,
  .size&lt;span class=&quot;token variable&quot;&gt;#{$size}&lt;/span&gt; &gt; video &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;--size&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;#{$value}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;functioning-without-javascript&quot;&gt;Functioning without JavaScript&lt;/h2&gt;
&lt;p&gt;Because this site is written with Astro, JavaScript is optional. If you have JavaScript disabled, I hide the settings button and replace it with an identical button that’s greyed out:&lt;/p&gt;
&lt;pre class=&quot;language-html&quot; data-language=&quot;html&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;#x3C;&lt;/span&gt;noscript&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;#x3C;&lt;/span&gt;button&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token comment&quot;&gt;&amp;#x3C;!-- .... --&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;#x3C;/&lt;/span&gt;button&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;

  &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;#x3C;&lt;/span&gt;style&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token style&quot;&gt;&lt;span class=&quot;token language-css&quot;&gt;
    &lt;span class=&quot;token selector&quot;&gt;.a11y&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;display&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; none&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;#x3C;/&lt;/span&gt;style&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;#x3C;/&lt;/span&gt;noscript&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;#x3C;&lt;/span&gt;A11y&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;&lt;span class=&quot;token namespace&quot;&gt;client:&lt;/span&gt;load&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The body has a &lt;code&gt;noscript&lt;/code&gt; class on it that sets default themes, accounting for the &lt;code&gt;prefers-color-scheme&lt;/code&gt; value. When the site loads with JavaScript enabled, the class is removed and the “real” one from localStorage is applied. I used Sass to generate the defaults:&lt;/p&gt;
&lt;pre class=&quot;language-scss&quot; data-language=&quot;scss&quot;&gt;&lt;code class=&quot;language-scss&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Color defaults&lt;/span&gt;
&lt;span class=&quot;token selector&quot;&gt;.noscript &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;@each&lt;/span&gt; &lt;span class=&quot;token selector&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$key&lt;/span&gt;, &lt;span class=&quot;token variable&quot;&gt;$value&lt;/span&gt; in &lt;span class=&quot;token variable&quot;&gt;$nitefall&lt;/span&gt; &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;--&lt;span class=&quot;token variable&quot;&gt;#{$key}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;#{$value}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token atrule&quot;&gt;&lt;span class=&quot;token rule&quot;&gt;@media&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token property&quot;&gt;prefers-color-scheme&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; light&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token selector&quot;&gt;.noscript &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;@each&lt;/span&gt; &lt;span class=&quot;token selector&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$key&lt;/span&gt;, &lt;span class=&quot;token variable&quot;&gt;$value&lt;/span&gt; in &lt;span class=&quot;token variable&quot;&gt;$nitelight&lt;/span&gt; &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;--&lt;span class=&quot;token variable&quot;&gt;#{$key}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;#{$value}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Color themes&lt;/span&gt;
&lt;span class=&quot;token selector&quot;&gt;.colorNitefall &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;@each&lt;/span&gt; &lt;span class=&quot;token selector&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$key&lt;/span&gt;, &lt;span class=&quot;token variable&quot;&gt;$value&lt;/span&gt; in &lt;span class=&quot;token variable&quot;&gt;$nitefall&lt;/span&gt; &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;--&lt;span class=&quot;token variable&quot;&gt;#{$key}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;#{$value}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(Also, that codeblock syntax highlighting looks pretty bad, I’ll get around to wrangling Prism with better colors later.)&lt;/p&gt;
&lt;h2 id=&quot;final-product&quot;&gt;Final product&lt;/h2&gt;
&lt;p&gt;You’re looking at it! If you find any issues, feel free to leave a comment below, and I’ll try and fix it when motivation arrives. Here’s hoping it’s stable…&lt;/p&gt;</content:encoded></item><item><title>One week modding the PlayStation 3</title><link>https://notnite.com/blog/ps3</link><guid isPermaLink="true">https://notnite.com/blog/ps3</guid><description>Alternatively, one week spent compiling RPCS3</description><pubDate>Tue, 13 Aug 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I recently bought a PlayStation 3. In 2024. No, this isn’t a joke.&lt;/p&gt;
&lt;p&gt;I was tired of emulating games on RPCS3 and figured that it was an interesting avenue for homebrew. Despite the saying that the PS3 “has no games”, there were a lot of interesting games I wanted to play, and I wanted to write code for it given the PS3 architecture is more similar to a supercomputer than a video game console.&lt;/p&gt;
&lt;p&gt;After consulting the lovely &lt;a href=&quot;https://consolemods.org/wiki/PS3:Buying_Guide&quot;&gt;ConsoleMods.org buying guide&lt;/a&gt;, I ended up picking up a 21XX series Slim model. I installed Evilnat 4.91 CFW on it and put an old 1TB SSD laying around in it (yes, it’s a bit of a waste since SATA 1, but it is an improvement no matter what).&lt;/p&gt;
&lt;p&gt;It’s a lovely little console, and I’ve been enjoying it heavily (Skate 3 has consumed my life), and I wanted to talk about the week I’ve spent learning about it.&lt;/p&gt;
&lt;h2 id=&quot;initial-setup--first-impressions&quot;&gt;Initial setup &amp;#x26; first impressions&lt;/h2&gt;
&lt;p&gt;Even though &lt;a href=&quot;https://en.wikipedia.org/wiki/PlayStation_3#Launch&quot;&gt;I am two months older than the PlayStation 3&lt;/a&gt;, I never actually owned one before. Growing up, I had a Wii (a quite early model!) and a Xbox 360 E (the final revision that looks sorta like a Xbox One), but I didn’t really play a lot of unique games on there. I played a lot of Mario Kart Wii and the LEGO games with my dad, Minecraft with my school friends, and Portal 2 as the first singleplayer game I beat; but I didn’t explore much of the library in the seventh generation of consoles.&lt;/p&gt;
&lt;p&gt;I’ve homebrewed my Wii and played around with it before, but I didn’t find it that interesting. I haven’t played a game on my Xbox 360 in years (all Xbox modders reading this post: get to work on those hypervisor exploits), just turning it on to look around once or twice. (Also, my Xbox 360 once had its internal storage corrupt and I lost all my saves, so that doesn’t particularly motivate me…)&lt;/p&gt;
&lt;p&gt;So, what is the console like? What is homebrewing it like? Well, I turned on the console for the first time and set it up. The previous owner updated it to the latest version and factory reset it before I purchased it, so I was already on 4.91 and ready to hack it. I followed the &lt;a href=&quot;https://consolemods.org/wiki/PS3:Getting_Started&quot;&gt;ConsoleMods.org guide&lt;/a&gt;, knowing that I verified the console’s serial number before purchasing it, and used PS3 Toolset to dump the NAND &amp;#x26; patch the flash.&lt;/p&gt;
&lt;p&gt;This step is very easy to do - you just have to go to a website on your PS3, watch in horror slash amazement as it somehow scans process memory of your PS3, and then hit a few buttons. Then, to install CFW, you just plug in a USB drive and install the firmware from it! A very handy feature of the PS3.&lt;/p&gt;
&lt;p&gt;I also grabbed &lt;a href=&quot;https://github.com/aldostools/webMAN-MOD&quot;&gt;webMAN&lt;/a&gt; afterwards, and then it was pretty much done. Some thoughts on the actual PS3, though:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;I &lt;em&gt;really&lt;/em&gt; like the XMB and how it’s laid out. I never owned any PlayStation console myself, just playing on my friends’ consoles, so being able to actually use it feels very nice.&lt;/li&gt;
&lt;li&gt;The concept of theming is so cute! Apparently you can make custom themes on the Internet using a tool Sony published, and I guess there’s a GUI wrapper for it that can do sound effects somehow. I tried making one using the FFXIV UI assets (since the actual FFXIV themes are kinda bad) but the sound effects were too loud. Damn you SE_UI.scd.&lt;/li&gt;
&lt;li&gt;That startup sound is good.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;babys-first-homebrew&quot;&gt;Baby’s first homebrew&lt;/h2&gt;
&lt;p&gt;There are two toolchains people use in the PS3 community for making homebrew:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The official Sony SDK (leaked to the public) which I will not be linking for obvious reasons&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/ps3dev/ps3toolchain&quot;&gt;ps3toolchain&lt;/a&gt; and &lt;a href=&quot;https://github.com/ps3dev/PSL1GHT&quot;&gt;PSL1GHT&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The commits on these repositories already look a little sad. Commits are few and far between and usually just fixes that were PR’d. But let’s not lose hope!&lt;/p&gt;
&lt;p&gt;ps3toolchain is designed for a Unix-like environment, but I use Windows on my desktop. I could’ve tried it through MinGW or used WSL, but my MinGW install is currently broken and my WSL doesn’t have internet (average day on my computer), so I just got out &lt;a href=&quot;https://notnite.com/blog/framework-laptop&quot;&gt;the laptop that is so laptop&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;It failed building the first try because of my Python install lacking a library. One line removal from my Nix config and &lt;code&gt;sudo dnf install python3&lt;/code&gt; later (this is what I get for using Nix), it seemed to just work. I made a little shell script to set up the environment variables and launched CLion through a terminal.&lt;/p&gt;
&lt;p&gt;Now, uh… this is where things go downhill. CLion was doing a bad job at understanding the toolchain and Makefile. I wanted to switch to a better build system, so I tried looking for CMake. &lt;a href=&quot;https://github.com/ps3dev/PSL1GHT/pull/134&quot;&gt;There’s a PR for it&lt;/a&gt;, but the executables built with it don’t run.&lt;/p&gt;
&lt;p&gt;Great. I tried making the CMake project myself, and ran into the same issue. Also tried Meson instead, and same issue. After a quick necropost I got “I did not, sadly” in response to asking if this was fixed. The PR remains closed as of today.&lt;/p&gt;
&lt;p&gt;This is a rocky start, but these are quality of life features, not absolute dealbreakers. With a normal Makefile, the programs still compile, so I guess I’ll just write code with a partially broken IDE.&lt;/p&gt;
&lt;p&gt;The samples built and ran on RPCS3, but I didn’t care enough to try them on console. Now, let’s start talking about what I really wanted to focus on: modding games!&lt;/p&gt;
&lt;h2 id=&quot;what-the-hell-is-an-eboot-anyway&quot;&gt;What the hell is an EBOOT, anyway&lt;/h2&gt;
&lt;p&gt;It seems like most developers hang out on a forum called &lt;a href=&quot;https://www.psx-place.com/&quot;&gt;PSX-Place&lt;/a&gt;. I don’t want to make an account for it, but browsing around and looking at the dates of posts already gives me the feeling I’m walking into a place far before my time. It’s the same kind of feeling I got playing FINAL FANTASY XI - this place, and the golden ages of its existence, was far before I even knew of it.&lt;/p&gt;
&lt;p&gt;Luckily, I didn’t need to swamp around in here for very long to figure out what I was doing. My good friend &lt;a href=&quot;https://github.com/InvoxiPlayGames&quot;&gt;Emma&lt;/a&gt; works on a mod for Rock Band 3 called &lt;a href=&quot;https://github.com/RBEnhanced/RB3Enhanced&quot;&gt;RB3Enhanced&lt;/a&gt; and she &lt;a href=&quot;https://github.com/InvoxiPlayGames/RB3Enhanced&quot;&gt;maintains a fork of it for experimental PS3 support&lt;/a&gt;. She gave me some pointers on the PS3:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Executables are ELF files. Most executables are &lt;a href=&quot;https://www.psdevwiki.com/ps3/SELF_-_SPRX&quot;&gt;SELFs&lt;/a&gt; (the S stands for signed), which is a wrapper around ELF files with encryption.&lt;/li&gt;
&lt;li&gt;Games boot through a SELF named EBOOT.BIN (compared to the previous PlayStation consoles’ BOOT.BIN). Some games use launchers that start other .selfs in the game directory, but most games just have their entire code in this single EBOOT.&lt;/li&gt;
&lt;li&gt;Libraries on the PS3 are PRX files, which are fancy ELFs. There is a signed equivalent (SPRX). System libraries, and game libraries (if any), are packaged in this format and you can execute code from them.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;PS3 game modding is primarily done through SPRX files. Developers will build a SPRX that gets loaded into the game process and then writes into executable memory to hook functions and do fun stuff.&lt;/p&gt;
&lt;p&gt;Now, next question: how do you load a SPRX?&lt;/p&gt;
&lt;h2 id=&quot;the-long-road-of-patching-executables&quot;&gt;The long road of patching executables&lt;/h2&gt;
&lt;p&gt;I did what I usually do for answering these questions: opening a search engine. The first result &lt;a href=&quot;https://www.psx-place.com/threads/how-do-i-patch-a-eboot-to-load-a-prx.42580/&quot;&gt;was a post asking this question with zero replies&lt;/a&gt;. I found lots of posts from the 2010s, usually kids modding various shooter games and making tutorials (text or YouTube video). I saw a reference to a certain “SPRX ELF Builder” tool. (&lt;strong&gt;Let me make this very important that I do not endorse this tool and it’s a random closed source executable packed with UPX DO NOT USE THIS HOLY SHIT&lt;/strong&gt;)&lt;/p&gt;
&lt;p&gt;This tool works by taking in a decrypted EBOOT.BIN (usually named EBOOT.elf), doing some unknown closed-source magic on it, and then outputting a patched version. This patched version would load a .sprx from the PS3 hard drive using the path the user input into it. But how does it &lt;em&gt;actually&lt;/em&gt; work? Well, time to reverse engineer it, I guess.&lt;/p&gt;
&lt;p&gt;I ended up downloading it in a VM and opening it in IDA (sorry to Vector 35 for not using the Binary Ninja license I got a few months ago). As said above, it was packed with UPX, so I unpacked it and looked inside. It seemed to not have many interesting strings, and it was hard to follow the Qt signals for the buttons, so I tried just running it and see what it does.&lt;/p&gt;
&lt;p&gt;I saw it create a file named “temporary_file.exe” and then freeze up. …What the hell? I opened that executable in IDA, and it turns out… this GUI is just a wrapper for someone else’s tool! This is part of the PS3 scene that annoys me - so many things taken and built on top one another, and along the way that credit and source gets lost.&lt;/p&gt;
&lt;p&gt;I ran it from the command line, picking a random EBOOT from my PS3, and saw the string “Error: gcc build string not found”. …What???&lt;/p&gt;
&lt;p&gt;I tried it on Rock Band 3, since Emma informed me it worked there. I ended up opening the original and patched versions in IDA and 010 Editor (a hex editor) to compare. It turns out that this program does two things:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Finds the GCC version string implanted into the executable by the compiler, and overwrites it with the path to your SPRX&lt;/li&gt;
&lt;li&gt;Adds some shellcode into the entrypoint to load the SPRX, using that string&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Clever, but cursed. It relying on the GCC version string like that was not great, because a handful of games I tried it on didn’t work because there wasn’t a version string.&lt;/p&gt;
&lt;p&gt;This tool obviously had flaws, and it seemed like it was my only option. This left me with only one idea: What if I made my own patcher?&lt;/p&gt;
&lt;h2 id=&quot;local-idiot-butchers-explanation-of-elf-file-format&quot;&gt;Local idiot butchers explanation of ELF file format&lt;/h2&gt;
&lt;p&gt;So. Let’s learn ELF files. (I used &lt;a href=&quot;https://en.wikipedia.org/wiki/Executable_and_Linkable_Format#File_layout&quot;&gt;Wikipedia&lt;/a&gt; as a reference for this, by the way).&lt;/p&gt;
&lt;p&gt;The file starts with a header that describes the target machine, architecture, endianness, and more. We can assume most of these values won’t ever change on the PS3, as all games target the same platform. There is also the fields for the offset and size of the program and section headers, which we’ll get into later.&lt;/p&gt;
&lt;p&gt;After this, there is the program header table. This contains a list of chunks in the executable file that describe how they should be loaded - location in the file, what address it should appear at in memory, size, etc.&lt;/p&gt;
&lt;p&gt;Then, the sections. Sections are just chunks of arbitrary data with some flags - usually there is a section for code, a section for data, and whatever else the compiler feels like adding.&lt;/p&gt;
&lt;p&gt;Finally, there is the section header table, which defines names and areas of sections (sorta like the program header). This information is pretty much useless at runtime, and is mainly used for linking, from my understanding.&lt;/p&gt;
&lt;p&gt;If we wanted to add custom code to this, we’d need to make our own section. The biggest problem is the program header would need to increase in size to add the new entry, shifting everything in the process. However, I mentioned that the main header contains the &lt;em&gt;offsets&lt;/em&gt; of the program/section headers. These can actually be anywhere in the file (unless the parser of the ELF file is bugged), as long as the offset in the main header points to it.&lt;/p&gt;
&lt;p&gt;Shifting everything in the executable may be possible to implement, but we can simply move the program header to the end of the file, and fill where it was originally with zeroes to not move anything.&lt;/p&gt;
&lt;p&gt;I tried using &lt;a href=&quot;https://lib.rs/crates/object&quot;&gt;the Rust library &lt;code&gt;object&lt;/code&gt;&lt;/a&gt; and &lt;a href=&quot;https://lief.re/&quot;&gt;the Python library &lt;code&gt;LIEF&lt;/code&gt;&lt;/a&gt;, but the former didn’t want to relocate the program header and the latter made RPCS3 fail to read the executable.&lt;/p&gt;
&lt;p&gt;So, guess what: we’re doing it all from scratch, baby!&lt;/p&gt;
&lt;h2 id=&quot;creating-sprxpatcher&quot;&gt;Creating SPRXPatcher&lt;/h2&gt;
&lt;p&gt;I ended up making my own tool, called &lt;a href=&quot;https://github.com/NotNite/SPRXPatcher&quot;&gt;SPRXPatcher&lt;/a&gt;, in C#. It’s under the MIT License and has worked with every game I’ve tested it with. But first, I want to talk about how it works.&lt;/p&gt;
&lt;p&gt;As explained above, I’m adding my own section, then moving the program header to the end of the file and filling where it originally was with zeroes. The code I use is some assembly that Emma wrote for me, and it takes the address of the SPRX path and does the required syscalls to load it into memory.&lt;/p&gt;
&lt;p&gt;The goal is pretty simple: assemble that code into shellcode, paste that shellcode into the executable at a certain address, put the inputted SPRX path next to the code in that section, and then modify the entrypoint to jump to that shellcode.&lt;/p&gt;
&lt;p&gt;The ELF header contains the address of the entrypoint, but on the PS3 it’s actually an entry in the table of contents. Reading that gives us the “real” address of the entrypoint, and we can write out some instructions to jump there:&lt;/p&gt;
&lt;pre class=&quot;language-cs&quot; data-language=&quot;cs&quot;&gt;&lt;code class=&quot;language-cs&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token return-type class-name&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;uint&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;BuildJump&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;uint&lt;/span&gt;&lt;/span&gt; address&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token class-name&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt;&lt;/span&gt; upper &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;ushort&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;address &lt;span class=&quot;token operator&quot;&gt;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;16&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token class-name&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt;&lt;/span&gt; lower &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;ushort&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;address &lt;span class=&quot;token operator&quot;&gt;&amp;#x26;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0xFFFF&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
        &lt;span class=&quot;token number&quot;&gt;0x3D600000&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;uint&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; upper&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// lis r11, upper&lt;/span&gt;
        &lt;span class=&quot;token number&quot;&gt;0x616B0000&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;uint&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; lower&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// ori r11, r11, lower&lt;/span&gt;
        &lt;span class=&quot;token number&quot;&gt;0x7D6903A6&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;                &lt;span class=&quot;token comment&quot;&gt;// mtctr r11&lt;/span&gt;
        &lt;span class=&quot;token number&quot;&gt;0x4E800420&lt;/span&gt;                 &lt;span class=&quot;token comment&quot;&gt;// bctr&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This takes more instructions than a &lt;code&gt;bl&lt;/code&gt; but allows us to reach the code no matter what, since there is a maximum jump limit (more on that later). It just writes the address into the r11 register, moves r11 into the counter register, and then branches to the counter register.&lt;/p&gt;
&lt;p&gt;I then save all the registers to the stack, load the SPRX, restore the registers, run the instructions I overwrote in the entrypoint, and jump back. This does have a pretty big issue that if the first few instructions in the entrypoint are a branch, it may fail, but I haven’t ran into yet so I have no reason to fix it right now.&lt;/p&gt;
&lt;p&gt;After the shellcode, I directly stick the string of the SPRX path there, so it’s all in the same section. I spent quite a while getting the ELF file to properly parse and export, and then getting the shellcode to work, but after about two days of effort: it works! To my knowledge, this is the first SPRX injector-patcher-thingy in recent times, and also maybe the first open source one? Unsure about that last one.&lt;/p&gt;
&lt;p&gt;I tested this with RB3Enhanced, both in emulator and on a real console, and it worked. I was impressed it worked on a real console, given that relocating the program header is a known hard spot for ELF file loading.&lt;/p&gt;
&lt;p&gt;Unfortunately, after this long, I didn’t actually have much energy to mod any games, but I did write a small hooking library which I want to talk about.&lt;/p&gt;
&lt;h2 id=&quot;hooking-functions-with-only-60-wasted-instructions&quot;&gt;Hooking functions with only 60 wasted instructions&lt;/h2&gt;
&lt;p&gt;In some games I tried modding, hooks would randomly fail and crash. Why? Well, it turns out that the executables of some games were so large that it would try and jump more than it could for its maximum size, and then underflow and jump backwards (into 0xFFFF____ space). I joked about solving this with a Backwards Long Jump - making a section in the executable allocated at 0xFFFF0000 and then doing a &lt;code&gt;bctr&lt;/code&gt; into my SPRX - but it was too much effort.&lt;/p&gt;
&lt;p&gt;I ended up taking a look at how RB3Enhanced does its hooking (and asked Emma), and found out a few things I need to know:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Register &lt;code&gt;r2&lt;/code&gt; on the PlayStation 3 contains the address of the table of contents. When transitioning from game code to mod code, or vice versa, this needs to be handled appropriately. Getting the mod TOC is very easy, but the game TOC is best fetched by making a hook early in game startup and then setting a static variable to the value of r2.&lt;/li&gt;
&lt;li&gt;As mentioned earlier, branches are a headache when hooking. I had to take special care to fix the branches if I encountered one.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The TOC was solved in RB3E with making a “stub function” that handled setting it, but it did a weird hack by copying the game’s TOC to solve having to fix it. I wanted to do a little better, so I would need two stub functions for this scenario. I came up with a very verbose, but functional hooking system:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A caller branches to the target function&lt;/li&gt;
&lt;li&gt;The target function’s first four instructions are overwritten with a &lt;code&gt;bctr&lt;/code&gt; to my stub&lt;/li&gt;
&lt;li&gt;Writes the value of the link register to an address in memory&lt;/li&gt;
&lt;li&gt;Sets the mod TOC&lt;/li&gt;
&lt;li&gt;Jumps to my detour function&lt;/li&gt;
&lt;li&gt;Sets the game TOC&lt;/li&gt;
&lt;li&gt;Restores the link register from the memory address&lt;/li&gt;
&lt;li&gt;Returns to the caller&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The link register is saved and restored so that we can make our way back to the caller. I store the link register right after the return instruction. This is not thread safe, and a &lt;em&gt;very&lt;/em&gt; verbose way of doing this, but it seems to be stable for my use cases.&lt;/p&gt;
&lt;p&gt;Hooks also sometimes need to call the original function, though. For this, I made a second stub:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Saves the link register (again)&lt;/li&gt;
&lt;li&gt;Sets the game TOC&lt;/li&gt;
&lt;li&gt;The four instructions from the target function (that were overwritten with the branch to the first stub)&lt;/li&gt;
&lt;li&gt;Jump to the fifth instruction of the function&lt;/li&gt;
&lt;li&gt;Sets the mod TOC&lt;/li&gt;
&lt;li&gt;Restores the link register (again)&lt;/li&gt;
&lt;li&gt;Returns to the detour&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I ended up needing to fix the branches here, as I said earlier in the section about SPRXPatcher. I did this by making four instructions for each instruction we overwrote, so sixteen instructions total. In the scenario I detected a branch, I would branch to that extra space and use the four instructions to &lt;code&gt;bctr&lt;/code&gt; to the branch. This is a very wasteful approach, but again, it functions!&lt;/p&gt;
&lt;p&gt;I ended up writing a few macros for this:&lt;/p&gt;
&lt;pre class=&quot;language-cpp&quot; data-language=&quot;cpp&quot;&gt;&lt;code class=&quot;language-cpp&quot;&gt;&lt;span class=&quot;token function&quot;&gt;DETOUR&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;function_name&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;/* return type */&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;void&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; self &lt;span class=&quot;token comment&quot;&gt;/* zero or more arguments */&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;LOG&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Meow! :3&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;function_name_orig&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;self&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;init_hooks&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;HOOK&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token comment&quot;&gt;/* function addr */&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; function_name&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Pretty nice!&lt;/p&gt;
&lt;h2 id=&quot;closing-thoughts&quot;&gt;Closing thoughts&lt;/h2&gt;
&lt;p&gt;I enjoy the PS3 a lot more than I thought I would. My girlfriend has been spending a lot of time playing games on it, while I’ve been sitting here bashing my head against PowerPC assembly. &lt;del&gt;What a cute dynamic.&lt;/del&gt;&lt;/p&gt;
&lt;p&gt;I want to give a lot of thanks to the &lt;a href=&quot;https://www.psdevwiki.com/ps3/&quot;&gt;PS3 Developer Wiki&lt;/a&gt; for a lot of knowledge, PSL1GHT and ps3toolchain (even if it hurt me internally to set up), Emma for her infinite wisdom, and a lot of my other nerd friends (a special shoutout to &lt;a href=&quot;https://github.com/floppydiskette&quot;&gt;husky&lt;/a&gt; and &lt;a href=&quot;https://aly.fish/en/&quot;&gt;Aly&lt;/a&gt; for helping me with some shellcode). I also stumbled upon &lt;a href=&quot;https://github.com/skiff/libpsutil&quot;&gt;libpsutil&lt;/a&gt;, which seems neat, but their hooking library obviously won’t work for me. :P&lt;/p&gt;
&lt;p&gt;Even though this scene is kind of fading away slowly as I speak, it makes me happy to know there are still a handful of passionate people around. People who want to build stuff for the curiosity of it, not because they see free games and go WOOOOOOO. While searching for information on how to make SPRX files, I found &lt;a href=&quot;https://www.reddit.com/r/ps3homebrew/comments/erg9nd/does_anyone_know_where_to_find_sprx_plugin/&quot;&gt;this Reddit post&lt;/a&gt;. To quote:&lt;/p&gt;
&lt;hr&gt;
&lt;blockquote&gt;
&lt;p&gt;I don’t know if I will. My intention was theoretical research. I was going to write stuff to gain knowledge and improve documentation. But - I’m sad to say this - the scene is in a very sorry state. Everyone is happy with how it is, because copied games can be run and that seems to be enough for most. So I’m really losing motivation.&lt;/p&gt;
&lt;p&gt;I cannot even find generic high-level information, such as what SPRX plugins are loaded by, and into what processes. The only good source of information is psdevwiki, but it mostly contains reverse engineered specifications, which are nice to have as a reference while you’re writing stuff, but are completely useless to get something up and running.&lt;/p&gt;
&lt;p&gt;It is absurd that there is so much homebrew software around written by a lot of different people, but all resources regarding how it was developed are already lost, after a few years of scene inactivity. I even tried going down the road of contacting a few active sceners, but some of the main communities are either offline or are not accepting new registrations.&lt;/p&gt;
&lt;p&gt;So much lost knowledge! It breaks my heart.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;p&gt;This comment resonated with me a lot. It was made &lt;em&gt;four years ago&lt;/em&gt;, and yet it still stands true. I call out to those in software development communities: please, please, &lt;em&gt;please&lt;/em&gt;, make sure your tools are available and archived. Make sure your methods and knowledge are preserved. It lowers the barrier to entry, and most importantly, it makes sure the second generation of your community can still press on.&lt;/p&gt;
&lt;p&gt;A console as old as me still brought me a lot of enjoyment this day, thanks to the friends I know who were willing to help me on my adventure. I am thankful that, even though the scene rots away in silence, there is still opportunity to learn and create and explore.&lt;/p&gt;</content:encoded></item><item><title>First experiences with the Framework Laptop</title><link>https://notnite.com/blog/framework-laptop</link><guid isPermaLink="true">https://notnite.com/blog/framework-laptop</guid><description>Beautiful women named Framework Laptop 13 in your area</description><pubDate>Thu, 02 May 2024 00:00:00 GMT</pubDate><content:encoded>&lt;details data-astro-cid-vxl4wgev=&quot;&quot;&gt;&lt;summary data-astro-cid-vxl4wgev=&quot;&quot;&gt;Note&lt;/summary&gt;&lt;p&gt;This article was written earlier this year (March 18th) but I forgot about it until today. I’ll let my original thoughts speak for itself instead of updating it.&lt;/p&gt;&lt;/details&gt;
&lt;p&gt;I bought a Framework Laptop (13-inch) a bit ago, and I’m absolutely in love with it. A friend encouraged me to write about it, so here’s what I did with it.&lt;/p&gt;
&lt;h2 id=&quot;configuration--checkout&quot;&gt;Configuration &amp;amp; checkout&lt;/h2&gt;
&lt;p&gt;I went with 32 GB of ram on the i5-1340P (13-inch 13th gen Intel), as the i7s didn’t seem to be as much of an improvement as they would cost. I’m a very big fan of the Expansion Cards system that is in use - I got two USB-C, two USB-A, and then spare SD card reader and HDMI cards in case I need them. I also got the DIY edition because it’s fun to assemble!&lt;/p&gt;
&lt;p&gt;Turns out that my email on my Framework account was wrong because of a typo. Oops! I emailed support about it - they eventually got around to it, but only after the laptop arrived, so I had to log in with the typo’d email to get tracking information.&lt;/p&gt;
&lt;p&gt;It arrived in 4 days, shipping from Taiwan to the eastern United States. The day before it arrived, the shipment stayed in a nearby city for the entire day, and I noted that on the FedEx website it indicated that the package was requested to be delivered on a weekday. Not great for me being impatient, but probably useful for someone.&lt;/p&gt;
&lt;h2 id=&quot;unboxing-and-assembly&quot;&gt;Unboxing and assembly&lt;/h2&gt;
&lt;p&gt;The laptop arrived in a giant box of boxes with all the parts required to assemble it (including the screwdriver). Negative points for no gummy bears.&lt;/p&gt;
&lt;p&gt;Opening the box itself, the laptop looks beautiful with no input cover or bezel on it. Every part is labeled with a QR code for repair information and sometimes some small text with extra info.&lt;/p&gt;
&lt;p&gt;Assembly was simple, but I got a bit nervous putting in the RAM (“is it supposed to push down that much”) and the input cover (“oh god oh fuck what if I break this cable”). The installation was mostly slotting pieces together and allowing magnets to do the rest. The right side of the input cover being intentionally not aligned to the case until the bottom screws were tightened confused me.&lt;/p&gt;
&lt;p&gt;My initial notes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;I’m a big fan of how the laptop looks, such that I haven’t even put any stickers on it yet.&lt;/li&gt;
&lt;li&gt;Having manual kill switches for both audio and video at the top is a very big plus for me.&lt;/li&gt;
&lt;li&gt;The keyboard feels surprisingly good! I expected struggle using it (given that I actively work with a split keyboard on my desk) but I was able to get accustomed to it almost instantly.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I plugged it in for charge, pressed the power button, and… the power indicator turned on and off. Uh oh!&lt;/p&gt;
&lt;p&gt;I read online causes and read that the battery may be unplugged. It seemed fine, though, so I just let it sit for a few minutes to charge and tried again. Worked, lol.&lt;/p&gt;
&lt;h2 id=&quot;installation&quot;&gt;Installation&lt;/h2&gt;
&lt;p&gt;I decided to go with Fedora (Workstation) for my operating system. I usually use NixOS, but I got quite tired of NixOS being NixOS, and I also wanted an excuse to use Fedora more.&lt;/p&gt;
&lt;p&gt;Basically everything worked out of the box on Fedora (as advertised). I copied my SSH and PGP key over with a USB drive and imported them, and then installed Nix and cloned my dotfiles repository. home-manager brought everything automatically, minus the fonts for some reason? I manually created a fonts directory and installed a Nerd Font into it, because I didn’t want to bother.&lt;/p&gt;
&lt;p&gt;I also had to create a custom session in GDM to get into awesome, and I had to tweak my configs to change my DPI/font size to match this screen. I then tried to open a terminal, and… nothing. Why?&lt;/p&gt;
&lt;p&gt;I ran kitty in another terminal (GNOME Terminal) to see what was up:&lt;/p&gt;
&lt;pre class=&quot;language-text&quot; data-language=&quot;text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;julian@thegame:~$ kitty
[078 13:14:11.662882] [glfw error 65542]: GLX: No GLXFBConfigs returned
[078 13:14:11.662893] [glfw error 65545]: GLX: Failed to find a suitable GLXFBConfig
[078 13:14:11.662897] Failed to create GLFW temp window! This usually happens because of old/broken OpenGL drivers. kitty requires working OpenGL 3.3 drivers.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After a fair bit of googling, and realizing this only happened on packages I had installed from nixpkgs (glxgears worked), I found &lt;a href=&quot;https://github.com/nix-community/nixGL&quot;&gt;nixGL&lt;/a&gt; and installed it. I modified my dotfiles to use it, with blatant disregard for compatibility with my other machines (lol).&lt;/p&gt;
&lt;p&gt;Most things worked out of the box. I also installed XIVLauncher and it managed to run FFXIV at a crisp 30 fps at 720p on low settings. That’s Intel for you!&lt;/p&gt;
&lt;h2 id=&quot;actually-using-it&quot;&gt;Actually using it&lt;/h2&gt;
&lt;p&gt;I haven’t been using the laptop for very long, but I’ve already got a few comments about its use.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It performs pretty badly in games (expectedly). The 16-inch model is likely a lot better for people that need a dedicated GPU for gaming or creative work.&lt;/li&gt;
&lt;li&gt;The screen isn’t that good in sunlight. I took it with me to a park in mid-day, and while it was usable, it wasn’t very easy to make out some things.&lt;/li&gt;
&lt;li&gt;The keyboard feels very nice for me with the exception of the Backspace key, which seems to… squeak??? What???&lt;/li&gt;
&lt;li&gt;The trackpad scrolls &lt;em&gt;wayyyy&lt;/em&gt; too fast for my liking. There’s probably a way to configure the speed in software but I dunno how.&lt;/li&gt;
&lt;li&gt;Brightness function keys didn’t work until I ran &lt;code&gt;sudo grubby --update-kernel=ALL --args=&amp;quot;module_blacklist=hid_sensor_hub&amp;quot;&lt;/code&gt;. Volume function keys only seem to work when I press them really fast and also have another key down(???).&lt;/li&gt;
&lt;li&gt;It compiles code about as fast as my own PC, which is impressive. Compiling &lt;a href=&quot;https://github.com/NotNite/pluggy&quot;&gt;pluggy&lt;/a&gt; both took around 11 seconds.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Setting up an automatic camera for my cat feeder</title><link>https://notnite.com/blog/gobbocam</link><guid isPermaLink="true">https://notnite.com/blog/gobbocam</guid><description>Because the world definitely needed more cat GIFs</description><pubDate>Sun, 28 Apr 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img alt=&quot;An orange cat eating food out of a bowl&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;626&quot; height=&quot;832&quot; src=&quot;https://notnite.com/_astro/goblin.5fFOpuVC_Z2dkkT6.webp&quot;&gt;&lt;/p&gt;
&lt;p&gt;This is Goblin, one of my cats. She is very stupid.&lt;/p&gt;
&lt;p&gt;In my house, we own an automatic cat feeder. This feeder has two bowls and dispenses kibble on a timer. A bonus of this feeder is that it has a camera on the front, allowing you to record motion of the cats eating and save it to a microSD card located inside of the feeder.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;A screenshot of the mobile app, showcasing the camera display&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;1080&quot; height=&quot;864&quot; src=&quot;https://notnite.com/_astro/petlibro-app.WRYzvrgs_Z2ebK9U.webp&quot;&gt;&lt;/p&gt;
&lt;p&gt;I wanted to take these videos and post them to my friends automatically, inspired by the &lt;a href=&quot;https://hellostreetcat.com/&quot;&gt;Hello Street Cat streams&lt;/a&gt; and &lt;a href=&quot;https://meow.camera/&quot;&gt;restreams&lt;/a&gt;. I researched the brand, coming to the conclusion that a previous model used &lt;a href=&quot;https://www.tuya.com/&quot;&gt;Tuya&lt;/a&gt; but the one I own uses a new server. I planned to hook it up to my Home Assistant instance, but nobody had written an integration yet.&lt;/p&gt;
&lt;p&gt;After a short bit of opening the mobile app in &lt;a href=&quot;https://github.com/skylot/jadx&quot;&gt;JADX&lt;/a&gt; and seeing the log messages being purely in Chinese, I decided I didn’t want to reverse engineer this protocol for the camera and I’d instead opt for a second one pointed at the feeder.&lt;/p&gt;
&lt;p&gt;I found a spare Raspberry Pi laying around, sacrificed an old webcam, and bought a microSD card at the local Target. After some quick tape on the nearest cat tree:&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;A Raspberry Pi and webcam sitting on top of a cat tree&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;1061&quot; height=&quot;799&quot; src=&quot;https://notnite.com/_astro/rpi.CUUYZy_Q_ZNbFsW.webp&quot;&gt;&lt;/p&gt;
&lt;h2 id=&quot;writing-all-of-the-code&quot;&gt;Writing all of the code&lt;/h2&gt;
&lt;p&gt;First of all, I knew that I didn’t want a 24/7 live stream like Hello Street Cat, given that this will be a stream of my kitchen and I don’t live alone. I enjoyed the model of motion detected clips that the mobile app for the feeder does, but I didn’t want it to detect &lt;em&gt;any&lt;/em&gt; motion if someone was passing by or it was a false positive. I decided that I would upload every clip for “staging” into a private Discord channel, and then require manual approvals to send to my friends.&lt;/p&gt;
&lt;p&gt;Luckily, the steps of both detecting motion and capturing clips can be solved by the program just called &lt;a href=&quot;https://motion-project.github.io/&quot;&gt;motion&lt;/a&gt;. This was a simple &lt;code&gt;sudo apt install motion&lt;/code&gt; on my Pi and it detected the USB camera without problem (presumably because of some driver in the Linux kernel carrying the support).&lt;/p&gt;
&lt;p&gt;After enabling the web control (and turning off localhost-only access) in the config file, I was able to see a preview of the camera from the web server on port 8081. It worked!&lt;/p&gt;
&lt;p&gt;The next step is to instruct motion to capture videos when it detects motion. This is as simple as setting &lt;code&gt;movie_output on&lt;/code&gt; in the config file. Reading the file, there is also &lt;code&gt;on_movie_end&lt;/code&gt; to specify a command to run when a movie finishes recording.&lt;/p&gt;
&lt;p&gt;The architecture I worked out was:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;motion records the movie, saves it as an .mkv, and calls my script with &lt;code&gt;on_movie_end&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;My script uses ffmpeg to encode the .mkv as a .gif&lt;/li&gt;
&lt;li&gt;The .gif gets uploaded to a local web server&lt;/li&gt;
&lt;li&gt;The web server uploads the .gif to a private Discord channel&lt;/li&gt;
&lt;li&gt;A Discord bot, running in the same codebase as the web server, detects approval (replying to the original message)&lt;/li&gt;
&lt;li&gt;The .gif is forwarded to a public channel&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I decided to go with TypeScript for this project, as it was a language I’ve written &lt;a href=&quot;https://github.com/NotNite/SproutTracker&quot;&gt;both web servers and Discord bots&lt;/a&gt; in. I didn’t really need type safety, and the compile steps take super long on the Pi, but I’m fine with it.&lt;/p&gt;
&lt;p&gt;The upload script looks pretty simple:&lt;/p&gt;
&lt;pre class=&quot;language-sh&quot; data-language=&quot;sh&quot;&gt;&lt;code class=&quot;language-sh&quot;&gt;&lt;span class=&quot;token shebang important&quot;&gt;#!/usr/bin/env sh&lt;/span&gt;
&lt;span class=&quot;token builtin class-name&quot;&gt;set&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-e&lt;/span&gt;

&lt;span class=&quot;token assign-left variable&quot;&gt;tmpfile&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;/tmp/nya&lt;span class=&quot;token variable&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;date&lt;/span&gt; +%s&lt;span class=&quot;token variable&quot;&gt;)&lt;/span&gt;&lt;/span&gt;.gif&quot;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# Clean up even if the connection fails&lt;/span&gt;
&lt;span class=&quot;token builtin class-name&quot;&gt;trap&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;rm -rf &lt;span class=&quot;token variable&quot;&gt;$tmpfile&lt;/span&gt;&quot;&lt;/span&gt; EXIT
&lt;span class=&quot;token comment&quot;&gt;# I stole this off of StackOverflow&lt;/span&gt;
ffmpeg &lt;span class=&quot;token parameter variable&quot;&gt;-y&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-i&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$1&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-vf&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;fps=10,scale=320:-1:flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse&quot;&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-loop&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$tmpfile&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# Upload it, making sure to retry if it fails (like the server restarting)&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;curl&lt;/span&gt; --connect-timeout &lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt; --max-time &lt;span class=&quot;token number&quot;&gt;10&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--retry&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;500&lt;/span&gt; --retry-delay &lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt; --retry-max-time &lt;span class=&quot;token number&quot;&gt;40&lt;/span&gt; http://localhost:8300/stage &lt;span class=&quot;token parameter variable&quot;&gt;-X&lt;/span&gt; POST &lt;span class=&quot;token parameter variable&quot;&gt;-H&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Content-Type: multipart/form-data&quot;&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-F&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;image=@&lt;span class=&quot;token variable&quot;&gt;$tmpfile&lt;/span&gt;&quot;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# Delete the original video to save space&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;rm&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-rf&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then, in the server, I just forward that .gif to a Discord bot (effectively using Discord as a CDN, lol):&lt;/p&gt;
&lt;pre class=&quot;language-ts&quot; data-language=&quot;ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;router&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;post&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;/stage&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;ctx&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; file &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; ctx&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;request&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;files&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;image&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;formidable&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;File&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; data &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;readFileSync&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;file&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;filepath&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; bot&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;createMessage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;config&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;stagingChannel&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    attachments&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        filename&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Date&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;now&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;.gif&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        file&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; data
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  ctx&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;status &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;204&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  ctx&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;body &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I note that formidable makes a copy of the file upload in &lt;code&gt;/tmp&lt;/code&gt;, which is literally exactly what I’m doing before I upload it, so I’m wasting double the memory by doing this (when I could just be passing the file path). But I don’t care.&lt;/p&gt;
&lt;p&gt;I then wrote a simple handler that checks when I reply to a gif the bot posted with the content “publish”. It then takes the gif and forwards it to the public channel, reacting with a checkmark to my message and the original message. If a reaction is already present on the gif from the bot (meaning it already uploaded it), it skips it and does nothing.&lt;/p&gt;
&lt;p&gt;For testing, I replayed a video that motion captured earlier that morning by calling the script manually. The result:&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;A screenshot of a Discord bot posting a cat gif&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;420&quot; height=&quot;246&quot; src=&quot;https://notnite.com/_astro/first-post.Da5e6_B5_ZxQ4g.webp&quot;&gt;&lt;/p&gt;
&lt;p&gt;It works!&lt;/p&gt;
&lt;h2 id=&quot;the-code-grows-and-grows&quot;&gt;The code grows and grows&lt;/h2&gt;
&lt;p&gt;My friends seem to really enjoy the concept of this. As I mentioned it to them, though, some asked to bring it to &lt;em&gt;their&lt;/em&gt; Discord server. Didn’t really plan for that, but I can do it! I changed the config file format to take a string array instead of a single string for the channel IDs to post in. It loops through every channel and attempts to post it.&lt;/p&gt;
&lt;p&gt;…Then, another friend asked to bring it into their server, but they didn’t want to invite the bot (given it had message content intents) and instead asked me to add webhook support. That’s okay! Just a simple &lt;code&gt;await fetch&lt;/code&gt;. …And then another friend asked for a webhook! Okay, yep, yeah, this is getting a little bit out of hand for a thing that posts gifs but I can make it support multiple webhooks too.&lt;/p&gt;
&lt;p&gt;And then a friend asked me to have the bot &lt;em&gt;DM them personally&lt;/em&gt; when a new gif is posted so they don’t miss it. Sigh. I planned ahead for this one and made it an array to start with.&lt;/p&gt;
&lt;p&gt;I also added integration with the Mastodon API, so anyone who wants to see it can &lt;a href=&quot;https://wetdry.world/@gobbocam&quot;&gt;follow my cats on the fediverse&lt;/a&gt;. I considered adding Twitter support, but I feared I didn’t have a spare phone number to use in case the account got locked. I also got asked to add Bluesky support but it looks like they don’t have the capability for custom gifs(?????).&lt;/p&gt;
&lt;p&gt;I was also asked for a website, but I don’t want to deal with another service running on another server (especially when my infrastructure is &lt;a href=&quot;https://save-nix-together.org/&quot;&gt;all Nix in the middle of current events&lt;/a&gt;). Maybe some day.&lt;/p&gt;
&lt;p&gt;It now takes like ten seconds to upload the gif everywhere, but I think it’s a fun idea. I hope people enjoy staring at my cats or something.&lt;/p&gt;</content:encoded></item><item><title>Converting Minecraft maps to Half-Life 1</title><link>https://notnite.com/blog/minecraft-to-goldsrc</link><guid isPermaLink="true">https://notnite.com/blog/minecraft-to-goldsrc</guid><description>Pretty sure that&apos;s not what they meant when they said &quot;Worldcraft&quot;</description><pubDate>Sun, 04 Feb 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Over the past week, I’ve been working on porting a Minecraft map to Half-Life 1 for &lt;a href=&quot;https://hl2.sh/&quot;&gt;my girlfriend&lt;/a&gt;’s birthday. I accomplished this with a mix of writing code (Rust, Python, C#), manual work in &lt;a href=&quot;https://jack.hlfx.ru/en/&quot;&gt;J.A.C.K.&lt;/a&gt;, and manual asset editing in &lt;a href=&quot;https://getpaint.net/&quot;&gt;paint.net&lt;/a&gt;. The code powering this map is open source on GitHub &lt;a href=&quot;https://github.com/NotNite/ketaminekeep&quot;&gt;here&lt;/a&gt;. I’ll update this post with a link to download the map when it’s ready for the public.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;An image of Ketamine Keep&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;1280&quot; height=&quot;720&quot; src=&quot;https://notnite.com/_astro/cover.dJBDzoMH_ZPPbu5.webp&quot;&gt;&lt;/p&gt;
&lt;h2 id=&quot;exporting-the-world&quot;&gt;Exporting the world&lt;/h2&gt;
&lt;p&gt;The original map is a large castle called Ketamine Keep that I built back in 2020 with my girlfriend and some friends. I don’t want to port the entire world, but rather the build itself, so I used a schematic. A schematic is a Minecraft modding term for a file that contains information about a structure. More specifically, I’ll be using the &lt;a href=&quot;https://github.com/maruohon/litematica&quot;&gt;Litematica&lt;/a&gt; mod.&lt;/p&gt;
&lt;p&gt;I have a world download saved on my file server, so I opened it up and started carving around it. It’s important to remove the extra ground below it, because it increases the amount of blocks inside of the build for no benefit.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Carving out the schematic&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;1280&quot; height=&quot;705&quot; src=&quot;https://notnite.com/_astro/schematic-creation.SgTvqTha_Zuj3lX.webp&quot;&gt;&lt;/p&gt;
&lt;p&gt;The Barrier block (an invisible block that blocks passage) can be used to represent clip brushes in the schematic, and then another one can be used for the skybox (I chose Tinted Glass):&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Creating the clip brushes and skybox&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;1280&quot; height=&quot;705&quot; src=&quot;https://notnite.com/_astro/clip-brushes.CSx_LoNf_Z6Lwz.webp&quot;&gt;&lt;/p&gt;
&lt;p&gt;I also removed the lights outside of the skybox (to prevent entity leak errors at build time) and killed the bats flying around beneath the ground. After setting up the build, I exported it to a .litematic file. This file format is gzip compressed NBT data. I attempted to parse it in Rust originally (using &lt;a href=&quot;https://lib.rs/crates/rustmatica&quot;&gt;rustmatica&lt;/a&gt;), but it was reporting incorrect block positions. I also tried parsing the NBT data by hand (using &lt;a href=&quot;https://lib.rs/crates/simdnbt&quot;&gt;simdnbt&lt;/a&gt;), but I didn’t understand it. To solve this, I just used the &lt;a href=&quot;https://pypi.org/project/litemapy/&quot;&gt;litemapy&lt;/a&gt; Python library to export it as a massive JSON file. Wasteful, but it works.&lt;/p&gt;
&lt;p&gt;After reading how to set up J.A.C.K., I used the lovely &lt;a href=&quot;https://github.com/pwitvoet/wadmaker&quot;&gt;WadMaker&lt;/a&gt; command line tool to pack some Minecraft textures for the map. The textures are .jpgs located in the Minecraft .jar file, so I found it on disk in my &lt;a href=&quot;https://prismlauncher.org/&quot;&gt;Prism Launcher&lt;/a&gt; libraries folder and opened it in &lt;a href=&quot;https://www.7-zip.org/&quot;&gt;7-Zip&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;The default J.A.C.K. map box with a cobblestone texture&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;1280&quot; height=&quot;720&quot; src=&quot;https://notnite.com/_astro/default-jack-map.eyOG2ce9_24705E.webp&quot;&gt;&lt;/p&gt;
&lt;p&gt;Pro tip: disabling texture filtering in the game settings helps the textures look not garbage. I did not learn that this existed until a bit later, so you’ll have to deal with muddy screenshots for now.&lt;/p&gt;
&lt;p&gt;Now comes porting the actual map!&lt;/p&gt;
&lt;h2 id=&quot;generating-the-map&quot;&gt;Generating the map&lt;/h2&gt;
&lt;p&gt;I’ll be outputting a .vmf from the script, which is a text-based format that represents the map. I chose to do this in Rust because it’s :rocket: blazingly fast :rocket: and good enough for the job.&lt;/p&gt;
&lt;p&gt;I made a blank .vmf in J.A.C.K. and opened it in a text editor, using it as a base. A basic .vmf contains some entities and a world object, which looks something like this:&lt;/p&gt;
&lt;pre class=&quot;language-text&quot; data-language=&quot;text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;world
{
  &quot;id&quot; &quot;0&quot;
  &quot;mapversion&quot; &quot;1&quot;
  &quot;classname&quot; &quot;worldspawn&quot;

  solid
  {
    &quot;id&quot; &quot;1&quot;
    side
    {
      &quot;id&quot; &quot;2&quot;
      (etc..)
    }
  }
}

entity
{
  &quot;id&quot; &quot;8&quot;
  &quot;classname&quot; &quot;info_player_start&quot;
  &quot;origin&quot; &quot;0 0 0&quot;
  &quot;angles&quot; &quot;0 0 0&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The world object contains all solid brushes, which each have multiple sides. Entities are not included in the world object. This seems remarkably similar to the VDF file format, from my experience. It’s JSON-like with loose children, so I guess it’s more like XML?&lt;/p&gt;
&lt;p&gt;I approached this by building a virtual voxel world in the program out of the schematic, which I’ll just represent as a &lt;code&gt;HashMap&amp;#x3C;Vector3, Voxel&gt;&lt;/code&gt; - where a Voxel contains the block ID and its block state (which is basically a key value dictionary).&lt;/p&gt;
&lt;p&gt;It’ll need to convert the coordinate space from Minecraft to GoldSrc, though. Googling, it seems that 1 Minecraft unit (1 meter) = roughly 40 GoldSrc units. Minecraft models operate in units of 16 per block, so I’ll go with 48 units to make it easily scalable by 3. GoldSrc is also Z up, so it needs to swap Y and Z.&lt;/p&gt;
&lt;p&gt;For now, let’s only worry about solid blocks and skip the rest. That means I can represent each block as a brush with six faces, and then output a .vmf with a light entity and an info_player_start. Filtering to only the stone bricks:&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;The stone brick walls open in J.A.C.K.&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;723&quot; height=&quot;361&quot; src=&quot;https://notnite.com/_astro/stone-brick-walls.CfTvPShd_Z2aIdAr.webp&quot;&gt;&lt;/p&gt;
&lt;p&gt;Seems to work, but this is very unoptimized, as each block on the wall is a single brush instead of one large brush. How can this be improved?&lt;/p&gt;
&lt;h2 id=&quot;writing-the-worlds-worst-greedy-meshing-algorithm&quot;&gt;Writing the world’s worst greedy meshing algorithm&lt;/h2&gt;
&lt;p&gt;I implemented a system known as “greedy meshing” for merging blocks together. The algorithm can be built in a very simple way: grow in one direction as much as possible, then the next, then the next. Repeat for every block in the mesh. You stop in one of these cases:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You hit a block with a different ID or properties&lt;/li&gt;
&lt;li&gt;You hit the edge of the schematic&lt;/li&gt;
&lt;li&gt;You hit another existing box&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I implement some weird version of flood fill on each block to group the same blocks into “clumps”, and then I run the greedy meshing algorithm on each clump. I also try all permutation of direction orders (X-&gt;Y-&gt;Z, Z-&gt;X-&gt;Y, Y-&gt;X-&gt;Z, etc.) and pick the one with the least brushes. This process only takes about two seconds to run on my computer in release thanks to &lt;a href=&quot;https://lib.rs/crates/rayon&quot;&gt;rayon&lt;/a&gt;, which does it all in parallel for me.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;A single wall brush for one of the sides of the castle&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;648&quot; height=&quot;368&quot; src=&quot;https://notnite.com/_astro/greedy-meshing.CzoGfp3s_61pa2.webp&quot;&gt;&lt;/p&gt;
&lt;p&gt;This is certainly not the best way to do this, and I probably got some terminology wrong, and it’s not really meshing since I just treat it as a giant box with two coordinates, but it works!&lt;/p&gt;
&lt;p&gt;I special cased the Barrier and Tinted Glass blocks to use the clip/sky textures as discussed earlier, and then opened it for the first compile.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;The first compile, with bad lighting&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;1920&quot; height=&quot;1080&quot; src=&quot;https://notnite.com/_astro/first-compile.DKoo1kNk_MgSMj.webp&quot;&gt;&lt;/p&gt;
&lt;p&gt;As expected, the lighting is bad and the textures look extremely bad. Blocks that don’t have their textures in the .wad are replaced with a light pink for now. I chose to focus on the textures and lighting.&lt;/p&gt;
&lt;h2 id=&quot;making-it-look-a-bit-better&quot;&gt;Making it look a bit better&lt;/h2&gt;
&lt;p&gt;Solving the lighting is simple: just use the lighting from the game itself! We placed down torches in the castle when originally building it, so I’ll just spawn a light entity where each torch is. I also used the lights.rad file to make the glowstone emit some light, and set up a light_environment, so everything should look a lot nicer after it.&lt;/p&gt;
&lt;p&gt;The textures look like they’re packed multiple times more into the block than they should be. From my understanding, this is because they’re 16x16, but the blocks are 48x48. Setting the UV scale to 3 (and aligning the V by 16 / 3 = ~5.33) seems to make the textures fit perfectly onto the brushes.&lt;/p&gt;
&lt;p&gt;I planned to do most of the non-full-block geometry by hand, but one small thing that’s easy to do is lower the path blocks slightly for immediately looking a bit better:&lt;/p&gt;
&lt;pre class=&quot;language-rust&quot; data-language=&quot;rust&quot;&gt;&lt;code class=&quot;language-rust&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; r#&lt;span class=&quot;token keyword&quot;&gt;box&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;id &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;dirt_path&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    max&lt;span class=&quot;token number&quot;&gt;.2&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-=&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;MC_TO_HAMMER&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;16&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I found on &lt;a href=&quot;https://minecraft.wiki/w/Dirt_Path#Usage&quot;&gt;the Minecraft wiki&lt;/a&gt; that the sizing was 15/16 units of a block, which can be double checked by opening the block .json:&lt;/p&gt;
&lt;pre class=&quot;language-json&quot; data-language=&quot;json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;from&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;to&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;16&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;15&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;16&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;adding-leaves-and-glass&quot;&gt;Adding leaves and glass&lt;/h2&gt;
&lt;p&gt;To do glass, just tie each glass brush to a func_breakable entity. It would be smarter to include all clumps in a single entity, but that required a bit of reworking in my code, so I ended up just fixing those cases by hand (given how little glass there is in the map). Leaves are a similar deal, but with func_illusionary.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;The glass blocks with a bullet hole in them&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;1280&quot; height=&quot;720&quot; src=&quot;https://notnite.com/_astro/glass.CJ5bodG0_QmrAO.webp&quot;&gt;&lt;/p&gt;
&lt;p&gt;For the transparency, WadMaker automatically handles it for us, as long as the texture filename starts with &lt;code&gt;{&lt;/code&gt;. What a perfectly sane and normal thing to do.&lt;/p&gt;
&lt;h2 id=&quot;handling-block-faces&quot;&gt;Handling block faces&lt;/h2&gt;
&lt;p&gt;The block state of a block looks something like this:&lt;/p&gt;
&lt;pre class=&quot;language-text&quot; data-language=&quot;text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;minecraft:furnace[facing=north,lit=false]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After stripping the block identifier and the brackets to just get &lt;code&gt;facing=north,lit=false&lt;/code&gt;, it can be split once more by commas and equal signs to form a dictionary. From this, I can just pick what texture I want for each face with a match statement:&lt;/p&gt;
&lt;pre class=&quot;language-rust&quot; data-language=&quot;rust&quot;&gt;&lt;code class=&quot;language-rust&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; props &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token namespace&quot;&gt;util&lt;span class=&quot;token punctuation&quot;&gt;::&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;parse_properties&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;#x26;&lt;/span&gt;r#&lt;span class=&quot;token keyword&quot;&gt;box&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;properties&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; face &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; props&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;facing&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token closure-params&quot;&gt;&lt;span class=&quot;token closure-punctuation punctuation&quot;&gt;|&lt;/span&gt;s&lt;span class=&quot;token closure-punctuation punctuation&quot;&gt;|&lt;/span&gt;&lt;/span&gt; s&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;as_str&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;unwrap_or&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;north&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; face &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;match&lt;/span&gt; face &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;&quot;north&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Face&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;North&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;&quot;east&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Face&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;East&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;&quot;south&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Face&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;South&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;&quot;west&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Face&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;West&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;&quot;up&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Face&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Top&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;&quot;down&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Face&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Bottom&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    _ &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token macro property&quot;&gt;unreachable!&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; side_id &lt;span class=&quot;token keyword&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;..&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;6&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; texture &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;match&lt;/span&gt; r#&lt;span class=&quot;token keyword&quot;&gt;box&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;id&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;as_str&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token string&quot;&gt;&quot;furnace&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; side_id &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Face&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Top&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;usize&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
                &lt;span class=&quot;token string&quot;&gt;&quot;furnace_top&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;to_string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; side_id &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; face &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;usize&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
                &lt;span class=&quot;token string&quot;&gt;&quot;furnace_front&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;to_string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
                &lt;span class=&quot;token string&quot;&gt;&quot;furnace_side&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;to_string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

        &lt;span class=&quot;token comment&quot;&gt;// more code here for other textures&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;// more code here for assembling the side&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;handling-models&quot;&gt;Handling models&lt;/h2&gt;
&lt;p&gt;The last thing I did before starting work on it manually was creating models automatically. To do this, I used &lt;a href=&quot;https://www.blockbench.net/&quot;&gt;Blockbench&lt;/a&gt; to convert the models to .objs manually, and then used &lt;a href=&quot;https://lib.rs/crates/tobj&quot;&gt;tobj&lt;/a&gt; to load it. I then converted the coordinates in the .obj into a .smd file and shelled out to StudioMDL to compile it.&lt;/p&gt;
&lt;p&gt;StudioMDL expects the textures to be 8 bit BMP files, and most libraries and tools I tried failed to convert it. It also requires that if transparency is used, it’s the last color of the BMP palette. I ended up writing a small C# script to convert them using System.Drawing, and manually cropped the textures with animations (like fire) to a single frame. Most textures were 16x16 and reused colors, so I never hit the color limit (and theoretically if I did I would’ve only needed to approximate one color).&lt;/p&gt;
&lt;p&gt;I forgot to translate Y up into Z up for the models, so I ended up rotating them in the entities in the map. Unfortunately, GoldSrc really did not like 1000 grass entities existing, and I started hitting weird render limits and entities phasing out of existence on recompiles/only being visible from certain blocks in the map. While I ended up not using this code as much as I wanted to, and it required a lot of manual effort, I still think it’s cool!&lt;/p&gt;
&lt;h2 id=&quot;what-was-done-by-hand&quot;&gt;What was done by hand&lt;/h2&gt;
&lt;p&gt;A lot more was done by hand:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;proper nodraw on faces that were touching other blocks&lt;/li&gt;
&lt;li&gt;fixing brush entities and clumps&lt;/li&gt;
&lt;li&gt;foliage, because models were unreliable&lt;/li&gt;
&lt;li&gt;fluids&lt;/li&gt;
&lt;li&gt;blocks with weird collisions (stairs, slabs, fences, ladders)&lt;/li&gt;
&lt;li&gt;block entities (chests)&lt;/li&gt;
&lt;li&gt;other spawns and items that made it a deathmatch map&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I also made a skybox, which I just used a random panorama screenshot mod off of Modrinth (because my own broke, lol). I took it in an empty world at noon, so it’s just the sky, some clouds, and the sun. Minecraft’s internal panorama code generates 6 square screenshots, so it was very easy to port (just getting the orientation right, resizing them, and making .bmp/.tga files).&lt;/p&gt;
&lt;h2 id=&quot;the-final-result&quot;&gt;The final result&lt;/h2&gt;
&lt;p&gt;We’re going to eventually play this map on my girlfriend’s birthday as a large chaotic party. While the code is in no way ready for other people to use it, I still published it on GitHub, in the hopes it’ll benefit someone as insane as me.&lt;/p&gt;</content:encoded></item><item><title>How I made Bomb Rush Cyberfunk multiplayer</title><link>https://notnite.com/blog/slop-crew</link><guid isPermaLink="true">https://notnite.com/blog/slop-crew</guid><description>A simple overview over my multiplayer mod, Slop Crew</description><pubDate>Thu, 30 Nov 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;On August 18th, &lt;a href=&quot;https://store.steampowered.com/app/1353230/Bomb_Rush_Cyberfunk/&quot;&gt;Bomb Rush Cyberfunk&lt;/a&gt; released on Steam. On August 23rd, I made my first commit to &lt;a href=&quot;https://github.com/SlopCrew/SlopCrew&quot;&gt;Slop Crew&lt;/a&gt;, a multiplayer mod I made for it. Let’s talk about how I made it!&lt;/p&gt;
&lt;p&gt;Keep in mind that the technical details here will be talking about the Slop Crew 2.0 rewrite, which has vastly different netcode compared to the original 1.0 (which was a WebSocket transporting a custom binary format, which sucked).&lt;/p&gt;
&lt;h2 id=&quot;1000-degree-glowing-knife-versus-unityplayerdll&quot;&gt;1000 degree glowing knife versus UnityPlayer.dll&lt;/h2&gt;
&lt;p&gt;BRC is a Unity game. In Unity, the code you write is a set of C# scripts and classes, which get compiled from their source code when the game is built. This compilation can output in one of two ways, depending on what method is chosen by the game developer.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Mono: The C# code is compiled into the &lt;a href=&quot;https://en.wikipedia.org/wiki/Common_Intermediate_Language&quot;&gt;Common Intermediate Language&lt;/a&gt; (which I’ll just call IL from now on), and is evaluated while the game is running by the Mono runtime (the C# runtime Unity uses).&lt;/li&gt;
&lt;li&gt;IL2CPP: The C# code is compiled into IL, but that IL is then transformed into C++ code, which is compiled into a native executable instead of being interpreted on the fly by the Mono runtime.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;IL2CPP is significantly harder to reverse, given it becomes a native binary; you’d have to use a decompiler like IDA or Ghidra to understand how the code functions. Thankfully, Team Reptile chose to use the Mono runtime for BRC, meaning it is significantly easier to reverse. Tools like &lt;a href=&quot;https://www.jetbrains.com/decompiler/&quot;&gt;JetBrains dotPeek&lt;/a&gt; can convert the IL code back into C# - an approximation, albeit - which makes understanding and interacting with it significantly easier.&lt;/p&gt;
&lt;p&gt;This allows us to read what the game is doing, but what about modifying it? We can use a plugin framework like &lt;a href=&quot;https://github.com/BepInEx/BepInEx&quot;&gt;BepInEx&lt;/a&gt;, which allows us to write C# code that has access to the Unity engine/game specific code, and then load that code at runtime when the game starts. We can use a library like &lt;a href=&quot;https://github.com/pardeike/Harmony&quot;&gt;Harmony&lt;/a&gt; to modify the game’s code, as well - without even having to touch the game files!&lt;/p&gt;
&lt;h2 id=&quot;wrangling-hundreds-of-players-efficiently&quot;&gt;Wrangling hundreds of players efficiently&lt;/h2&gt;
&lt;p&gt;Inside the game’s code, there exists a method called &lt;code&gt;SetupAIPlayerAt&lt;/code&gt;. This method creates a player object normally intended for the game’s AI, but we can instead hijack it to create a player for us, and then puppet it ourselves. But first, how are we going to communicate with other players?&lt;/p&gt;
&lt;p&gt;Slop Crew implements a client-server model, such that multiple clients (in this case, multiple instances of the Slop Crew BepInEx plugin) connect to a single server, and the server exchanges messages between them. The client and server are both written in C#.&lt;/p&gt;
&lt;p&gt;For networking, Slop Crew uses Valve’s &lt;a href=&quot;https://github.com/ValveSoftware/GameNetworkingSockets&quot;&gt;GameNetworkingSockets&lt;/a&gt;, a library created as a standalone alternative to Steam’s networking systems that can be used in developers’ games. It is more intended for people making new games rather than trying to staple multiplayer onto a singleplayer one, but it’ll work. GameNetworkingSockets (which I’ll just call GNS from now on) is written in C++, but our plugin &amp;#x26; server are written in C#. I found &lt;a href=&quot;https://github.com/nxrighthere/ValveSockets-CSharp&quot;&gt;a C# wrapper around it&lt;/a&gt; on GitHub and modified it to my needs.&lt;/p&gt;
&lt;p&gt;This has a problem in that the game is Windows-only, so the plugin will only ever run on native Windows or through Wine; but the server needs to run on both Windows and Linux natively. Thankfully, with a little bit of hackery to specify what platform it’s being compiled for (related to struct packing), we can make it run fine on both Windows and Linux.&lt;/p&gt;
&lt;p&gt;We have a way to transport data between the plugin and server, but what data will we transport? In this case, we need a protocol, which I settled on &lt;a href=&quot;https://github.com/protocolbuffers/protobuf&quot;&gt;Protocol Buffers&lt;/a&gt; (or just Protobuf for short). I can write my network schema in Protobuf, and then generate C# code to use that schema with. This means I could’ve picked other languages for the server, but I felt that writing it in C# was the best option (also because the 1.0 server was in C# as well).&lt;/p&gt;
&lt;h2 id=&quot;everything-is-a-state-machine-if-you-look-at-it-hard-enough&quot;&gt;Everything is a state machine if you look at it hard enough&lt;/h2&gt;
&lt;p&gt;We have a transport and protocol, but we need to exchange data between the plugin and server, along with managing their state on both sides. For this, I made three files for each part of the network: shared data, messages sent to the client, and messages sent to the server. This allows me to define a player in the network like this:&lt;/p&gt;
&lt;pre class=&quot;language-protobuf&quot; data-language=&quot;protobuf&quot;&gt;&lt;code class=&quot;language-protobuf&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;message&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Player&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;optional&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;uint32&lt;/span&gt; id &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt; name &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token positional-class-name class-name&quot;&gt;Transform&lt;/span&gt; transform &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token positional-class-name class-name&quot;&gt;CharacterInfo&lt;/span&gt; character_info &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;repeated&lt;/span&gt; &lt;span class=&quot;token positional-class-name class-name&quot;&gt;CustomCharacterInfo&lt;/span&gt; custom_character_info &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token builtin&quot;&gt;bool&lt;/span&gt; is_community_contributor &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;6&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;optional&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt; representing_crew &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;7&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;message&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;ServerboundHello&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token positional-class-name class-name&quot;&gt;Player&lt;/span&gt; player &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;optional&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt; key &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token builtin&quot;&gt;int32&lt;/span&gt; stage &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When a player connects to Slop Crew and loads into the game, it sends a &lt;code&gt;ServerboundHello&lt;/code&gt; packet to the server, which contains information about the player and the stage they’re in. The server groups players together by stage, which is just a number that corresponds to the ID inside the game. After assigning the player to a stage, it gives an updated list of all players in that stage to all players in that stage (effectively a list of players sent to every player in that list).&lt;/p&gt;
&lt;p&gt;When the plugin receives this packet, it looks through to see what players are new/missing/updated, and handles it appropriately. The plugin creates an &lt;code&gt;AssociatedPlayer&lt;/code&gt; class, which effectively ties the player object in the scene to the player object in the network.&lt;/p&gt;
&lt;p&gt;Whenever a player’s character does something like play an animation or switch outfits/characters, it sends a packet to the server that then relays that packet to everyone else in the stage. For example, the &lt;code&gt;PlayAnim&lt;/code&gt; method in the game’s code is hooked with Harmony to detect when it’s called for the current player, and then sends a packet to the server to relay to everyone else, playing the animation on everyone else’s client.&lt;/p&gt;
&lt;h2 id=&quot;managing-score-battles-and-races&quot;&gt;Managing score battles and races&lt;/h2&gt;
&lt;p&gt;The game has a concept of an “encounter” internally, for stuff like score battles in the game’s story. Slop Crew has its own encounter system, where both the client and server keeps track of its state. We use the in game phone to start encounters (which we were the first mod to add a custom phone app to!), which we hijack by simply hooking when it’s initialized and adding a custom app (along with bashing an app entry into the home screen).&lt;/p&gt;
&lt;p&gt;When a player requests to battle another player, the server keeps track of that request and sends a notification to the other player. If the other player responds to that notification (sending another request), the server will detect both players have requested to battle each other, and put them into an encounter. Encounters have state managed on both the client and server that gets synchronized, but usually things like timers run independent of each other. When in a score battle, the plugin sends a packet with its updated score to the server, and the server routes that packet to the appropriate running encounter and handles it (updating the score and telling the other player).&lt;/p&gt;
&lt;p&gt;Races are slightly more complicated in that the server also picks a random configuration for the race and serializes it over the network, which the plugin then spawns checkpoint objects into the game scene with.&lt;/p&gt;
&lt;h2 id=&quot;getting-data-in-and-out-of-other-plugins&quot;&gt;Getting data in and out of other plugins&lt;/h2&gt;
&lt;p&gt;Slop Crew has a separate &lt;a href=&quot;https://github.com/SlopCrew/SlopCrew/tree/main/SlopCrew.API&quot;&gt;API assembly&lt;/a&gt; which other BepInEx plugin developers can use to receive info about the current connection. This also exposes an API to let people send custom packets to the stage, to add integration into their own plugin.&lt;/p&gt;
&lt;p&gt;Slop Crew also uses some other plugins’ APIs, too - notably the &lt;a href=&quot;https://github.com/SGiygas/CrewBoomAPI&quot;&gt;CrewBoom API&lt;/a&gt;, which is used for syncing custom characters. Note that custom characters are only applied if you have the same character installed locally (would rather not repeat some FFXIV moon related mistakes).&lt;/p&gt;
&lt;h2 id=&quot;running-more-services-on-it-too&quot;&gt;Running more services on it, too&lt;/h2&gt;
&lt;p&gt;The Slop Crew server also runs an HTTP API with ASP.NET Core inside of it. This is used for linking your Discord account to your in game character - the server implements the Discord OAuth flow and then generates a unique key to insert into your config file. The frontend for the website is written in React and talks to the API running on the server, which saves information to a SQLite database. When a player connects and their plugin provides an authentication key, the server looks it up to find the account tied to it.&lt;/p&gt;
&lt;p&gt;The Slop Crew server also has support for Graphite metrics, which I use to connect to &lt;a href=&quot;https://sloppers.club/stats&quot;&gt;a Grafana instance&lt;/a&gt; for player activity. This component is completely optional, but it’s fun to stare at the statistics. Right now, it only tracks population of a given state, total connection count, and how many people are using a certain version of the Slop Crew plugin.&lt;/p&gt;
&lt;h2 id=&quot;ensuring-everything-is-safe--hostable&quot;&gt;Ensuring everything is safe &amp;#x26; hostable&lt;/h2&gt;
&lt;p&gt;The Slop Crew server is built on &lt;a href=&quot;https://github.com/features/actions&quot;&gt;GitHub Actions&lt;/a&gt; and uploads a Docker container to &lt;a href=&quot;https://github.com/features/packages&quot;&gt;GitHub Packages&lt;/a&gt;. The plugin is also built on GitHub Actions, and automatically does releases to GitHub and Thunderstore when a new tag is created. This means that the Slop Crew plugin needs to have access to the game files in order to build against the game code. To solve this, I use &lt;a href=&quot;https://github.com/BepInEx/BepInEx.AssemblyPublicizer&quot;&gt;the BepInEx assembly publicizer&lt;/a&gt; to create a stripped down version of the assembly, with no code in the methods - effectively equivalent to a header file. This is downloaded from a web server when the plugin is built.&lt;/p&gt;
&lt;p&gt;Slop Crew is 100% open source software, and I try my best to make it so that anyone can selfhost it. The Docker container is what’s used in production, but anyone can use the container and modify the Docker Compose file.&lt;/p&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Hopefully some of you enjoyed that. Slop Crew is a pretty big monolith of code now - over 10,000 lines, apparently! - but it certainly doesn’t feel that way to me. At its core, it’s a very simple set of systems connected to one another. If you liked reading that, but haven’t played the mod before, consider &lt;a href=&quot;https://sloppers.club/&quot;&gt;hopping online&lt;/a&gt; sometime!&lt;/p&gt;</content:encoded></item><item><title>How to get code execution from FFXIV mods in one simple step</title><link>https://notnite.com/blog/ffxiv-modloader-ace</link><guid isPermaLink="true">https://notnite.com/blog/ffxiv-modloader-ace</guid><description>A weave of bad ideas, one into another</description><pubDate>Wed, 29 Mar 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Some bold text to start off with:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;This vulnerability is patched in the latest version of Penumbra and TexTools.&lt;/strong&gt; If you aren’t up to date (Penumbra 0.6.4.0+ and TexTools 2.3.8.5+), do it now. TexTools users will have to switch to the beta branch to get the latest version. Additionally, Mare Synchronos likely was never able to sync/execute this (and now can’t with the Penumbra fix).&lt;/p&gt;
&lt;p&gt;Now, let’s explain what the vulnerability is. This vulnerability allows an attacker to build a specially crafted mod that would execute arbitrary code on file load, abusing what is fundamentally a bug in the base engine of FFXIV. By overwriting a certain path in the game’s filesystem, you could load code on game startup.&lt;/p&gt;
&lt;p&gt;So, how was it possible?&lt;/p&gt;
&lt;h2 id=&quot;never-trust-your-lua-state&quot;&gt;Never trust your Lua state&lt;/h2&gt;
&lt;p&gt;Yep! Lua!&lt;/p&gt;
&lt;p&gt;FFXIV uses Lua 5.1 to script a lot of things in the game. It doesn’t store just raw .lua files, though - it stores the weirdly-obscure Lua bytecode format (compiled with a 32-bit &lt;code&gt;luac&lt;/code&gt;). These are in the &lt;code&gt;game_script&lt;/code&gt; category in the game’s virtual filesystem, under the &lt;code&gt;.luab&lt;/code&gt; file extension.&lt;/p&gt;
&lt;p&gt;By writing custom code, compiling it into bytecode, and replacing files with it, your code can run in FFXIV’s Lua state! Oof.&lt;/p&gt;
&lt;p&gt;And, if you modify the event handler Lua script, your code can run on startup! I found this path by replacing every known Lua script in the game (lol) with a script that would write the filename to a text file on my desktop. Just be careful when doing this, as the event handler is used a &lt;em&gt;lot&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;…wait a minute, write to a text file? Does that mean they- Oh no.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;They left &lt;code&gt;os&lt;/code&gt; and &lt;code&gt;io&lt;/code&gt; in the Lua global table.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;YEP! This means you can read and write files and execute arbitrary programs! It’s literally this easy:&lt;/p&gt;
&lt;pre class=&quot;language-lua&quot; data-language=&quot;lua&quot;&gt;&lt;code class=&quot;language-lua&quot;&gt;os&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;execute&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&amp;quot;calc.exe&amp;quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
os&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;exit&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then you create the bytecode, swap the file, and you’re done! Calculator popped.&lt;/p&gt;
&lt;h2 id=&quot;what-you-could-do-with-this&quot;&gt;What you could do with this&lt;/h2&gt;
&lt;p&gt;There’s a lot of fun to be had with modifying Lua! You could:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Write a malicious payload to a file and run it&lt;/li&gt;
&lt;li&gt;Break cutscenes, menus, and much more&lt;/li&gt;
&lt;li&gt;Mess with the internals of FFXIV (spawn objects, play cutscenes, maybe even send invalid data to the server!)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Overall I haven’t explored the Lua state as much as I want to. The primary worry, of course, is executing malicious payloads from mod authors. This would have about the same amount of danger as running &lt;code&gt;totally_not_a_virus.mp4.exe&lt;/code&gt; off of some shady corner of the Internet (it could do anything you can do).&lt;/p&gt;
&lt;h2 id=&quot;some-tactics-featuring-dlls&quot;&gt;Some tactics, featuring DLLs&lt;/h2&gt;
&lt;p&gt;Unfortunately, &lt;code&gt;package.cpath&lt;/code&gt; is set to load DLLs from the &lt;code&gt;game&lt;/code&gt; folder. This means not only can you run arbitrary code, you can run arbitrary code inside of the FFXIV process! With a little work in Rust (featuring the &lt;code&gt;ctor&lt;/code&gt; crate), and storing the DLL as a byte array in the script (then writing it to a temporary file), we get free process access. We’re basically as powerful as a third party Dalamud plugin, which has big red text when installing it for a reason.&lt;/p&gt;
&lt;p&gt;With enough effort, we can get the player’s &lt;em&gt;session ID&lt;/em&gt; - the value that represents you when you log into the game - and do whatever with it. An attacker with this in their hands can log in as you!&lt;/p&gt;
&lt;p&gt;Here’s a little proof of concept showing off viewing the user’s session ID:&lt;/p&gt;
&lt;div class=&quot;video&quot; data-astro-cid-7qzxku2k=&quot;&quot;&gt; &lt;video controls=&quot;&quot; src=&quot;https://notnite.com/blog/res/ffxiv-modloader-ace/poc.mp4&quot; data-astro-cid-7qzxku2k=&quot;&quot;&gt;&lt;/video&gt; &lt;/div&gt;
&lt;h2 id=&quot;how-to-blend-in&quot;&gt;How to blend in&lt;/h2&gt;
&lt;p&gt;While TexTools prompts you with every file, nobody really &lt;em&gt;reads&lt;/em&gt; that. Stuff a bunch of unrelated files in there that mask the .luab and you’re good to go. Because sites like XIV Mod Archive don’t host mod files themselves, they can’t automatically scan for malicious files, meaning it has to be done in the modloader. &lt;a href=&quot;https://heliosphere.app/&quot;&gt;Heliosphere&lt;/a&gt; is the only mod site I know of that blocks certain file extensions automatically on upload.&lt;/p&gt;
&lt;h2 id=&quot;the-lowest-hanging-fruit-possible&quot;&gt;The lowest hanging fruit possible&lt;/h2&gt;
&lt;p&gt;I’m aware that this “vulnerability” is &lt;em&gt;incredibly&lt;/em&gt; low hanging fruit. Sorry to the people that thought I found a vulnerability in file parsing or something.&lt;/p&gt;
&lt;p&gt;Some may view me publishing this as immature and stupid, and causing shit for no good reason, but I can’t in good conscience keep this a secret. Just because nobody has exploited this yet doesn’t mean it won’t happen. Get your stuff patched!&lt;/p&gt;
&lt;h2 id=&quot;timeline&quot;&gt;Timeline&lt;/h2&gt;
&lt;p&gt;All times Eastern Time.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;01/20: I realize this vulnerability is conceptually possible&lt;/li&gt;
&lt;li&gt;01/21, 4:48 PM: I execute &lt;code&gt;calc.exe&lt;/code&gt; from inside FFXIV using Penumbra&lt;/li&gt;
&lt;li&gt;01/22, 10:39 AM: &lt;a href=&quot;https://github.com/xivdev/Penumbra/commit/e6d73971e99def23848a826e5c6a9c57cb303f5c#diff-5f735232cb3a01823caa0cb6a0ff03b158274196d94c4495dde698401fcc4ce7R144-R147&quot;&gt;Ottermandias commits to Penumbra to fix this&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;01/22, 8:19 PM: I execute &lt;code&gt;calc.exe&lt;/code&gt; from inside FFXIV using TexTools&lt;/li&gt;
&lt;li&gt;01/22, 11:33 PM: I submit a proof of concept to PrettyKitty, owner of the TexTools Discord server&lt;/li&gt;
&lt;li&gt;01/30, 12:07 PM: The Penumbra stable channel updates to patch the vulnerability&lt;/li&gt;
&lt;li&gt;03/03, 11:17 AM: After 40 days, I contact PrettyKitty for a status update&lt;/li&gt;
&lt;li&gt;03/27, 11:59 AM: I reach out to PrettyKitty one final time&lt;/li&gt;
&lt;li&gt;03/29, 09:42 AM: &lt;a href=&quot;https://github.com/TexTools/FFXIV_TexTools_UI/releases/tag/v2.3.8.5&quot;&gt;TexTools is updated to patch the vulnerability&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Update: 02/29, 7:36 PM:&lt;/strong&gt; Seeing some people attack the TexTools team for their response time - please don’t! They’re doing this in their free time (volunteer work) and I was informed that most were busy with other stuff. I also (with permission) revealed that my contact wasn’t a moderator but actually PrettyKitty, who informed me my messages were delivered to the developers very quickly.&lt;/p&gt;
&lt;h2 id=&quot;closing&quot;&gt;Closing&lt;/h2&gt;
&lt;p&gt;Penumbra and TT should’ve been blocking modifying these files for a very long time. I just so happened to be the first skid to put two and two together and break something! Good thing it’s fixed now. Maybe it was good that the first person to shout about this was me, because I’m not a malicious actor - I’m just an idiot.&lt;/p&gt;
&lt;p&gt;I haven’t seen this exploited in the wild, but I am concerned that it could be (given that this post is essentially a how-to). Make sure to update your modloaders!&lt;/p&gt;
&lt;p&gt;This “fun afternoon project” wouldn’t have been possible without:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/lmcintyre&quot;&gt;Perch&lt;/a&gt;’s &lt;a href=&quot;https://rl2.perchbird.dev/&quot;&gt;ResLogger2&lt;/a&gt; for making it easy to yoink the Lua script file list&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://anna.lgbt/&quot;&gt;Anna&lt;/a&gt;’s excellent &lt;a href=&quot;https://git.anna.lgbt/ascclemens/ttmp-rs&quot;&gt;TTMP Rust library&lt;/a&gt; for making my TexTools testing easier, and &lt;a href=&quot;https://heliosphere.app/&quot;&gt;Heliosphere&lt;/a&gt; for making it easy to avoid malicious files&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/Ottermandias&quot;&gt;Otter&lt;/a&gt; for fixing the Penumbra vulnerability fast and making modding painless&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/Pohky&quot;&gt;Pohky&lt;/a&gt; for telling me how fucked the Lua state was in the first place&lt;/li&gt;
&lt;li&gt;The people who have built our decade of research into FFXIV for satisfying my autistic curiosity (too many to count)&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>The GShade tango continues</title><link>https://notnite.com/blog/gshade-tango-2</link><guid isPermaLink="true">https://notnite.com/blog/gshade-tango-2</guid><description>Don&apos;t stop me nowwwwwwww</description><pubDate>Tue, 07 Feb 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;If you haven’t read the first one, &lt;a href=&quot;https://notnite.com/blog/gshade-tango&quot;&gt;do that&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;enter-deezshade&quot;&gt;Enter DeezShade&lt;/h2&gt;
&lt;p&gt;After the password protected .zip file, I had the idea of &lt;em&gt;using the installer itself to download the files&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;I made &lt;a href=&quot;https://git.n2.pm/NotNite/DeezShade&quot;&gt;DeezShade&lt;/a&gt; - it reflects the official installer with C# magic to download the files without the password. Pretty funny loophole.&lt;/p&gt;
&lt;h2 id=&quot;marot-gets-mad---the-sequel&quot;&gt;Marot gets mad - the sequel&lt;/h2&gt;
&lt;p&gt;So, I woke up the next morning, and guess what?&lt;/p&gt;
&lt;div class=&quot;center&quot;&gt;
  &lt;blockquote class=&quot;twitter-tweet&quot;&gt;&lt;p lang=&quot;en&quot; dir=&quot;ltr&quot;&gt;Yes, this is the kind of bullshit that GShade is working on, rather than bothering to implement a more sensible update system. The installer RESTARTS YOUR SYSTEM if it detects it&apos;s not being run &quot;properly&quot;. Continued... &lt;a href=&quot;https://t.co/3eacb6RH5U&quot;&gt;pic.twitter.com/3eacb6RH5U&lt;/a&gt;&lt;/p&gt;— perchbird (@perchbird_) &lt;a href=&quot;https://twitter.com/perchbird_/status/1622597904295682048?ref_src=twsrc%5Etfw&quot;&gt;February 6, 2023&lt;/a&gt;&lt;/blockquote&gt; 
&lt;/div&gt;
&lt;p&gt;I, uh, yep - Marot put malware in the GShade installer.&lt;/p&gt;
&lt;p&gt;Specifically, it detects if &lt;code&gt;App._instReady&lt;/code&gt; is set to false (which it would be on the official installer and not on DeezShade), and if so, SHUTS DOWN YOUR COMPUTER. The code looks like this:&lt;/p&gt;
&lt;pre class=&quot;language-cs&quot; data-language=&quot;cs&quot;&gt;&lt;code class=&quot;language-cs&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;token return-type class-name&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;void&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;lol&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token constructor-invocation class-name&quot;&gt;Process&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  StartInfo &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    WorkingDirectory &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; Environment&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;SystemDirectory&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    FileName &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;shutdown.exe&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    Arguments &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;-r -t 0&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    WindowStyle &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; ProcessWindowStyle&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Hidden
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;Start&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// later on in the code...&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;token return-type class-name&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;void&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;InitialPermissionsProcess&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;App&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;_instReady&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    App&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;lol&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// other stuff here, omitted for reading pleasure&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I think(?) this is against the Computer Fraud and Abuse Act, but I’m not a lawyer. Lawyers, tell me: &lt;code&gt;idkthelaw@(this domain)&lt;/code&gt;&lt;/p&gt;
&lt;h2 id=&quot;the-ultimate-confrontation&quot;&gt;The ultimate confrontation&lt;/h2&gt;
&lt;p&gt;After patching it out in DeezShade by setting that variable + using Harmony to patch the shutdown code, I decided to confront Marot about it in the GPOSERS Discord server.&lt;/p&gt;
&lt;p&gt;After being called a kindergartener by the person who accused me of conspiracy to commit a crime, Marot showed up and left me the ultimate message:&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Marot&amp;amp;#x27;s message&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;1330&quot; height=&quot;339&quot; src=&quot;https://notnite.com/_astro/message.DhYS-LOv_2dVFRm.webp&quot;&gt;&lt;/p&gt;
&lt;p&gt;…Wow.&lt;/p&gt;
&lt;p&gt;He… he put malware in his software… as a “lesson”. HE WROTE MALWARE TO GET BACK AT A 16 YEAR OLD. LMAO.&lt;/p&gt;
&lt;h2 id=&quot;the-fallout&quot;&gt;The fallout&lt;/h2&gt;
&lt;p&gt;After this, multiple events happened:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The community ditched GShade&lt;/li&gt;
&lt;li&gt;Marot gets banned from the r/FFXIV Discord server&lt;/li&gt;
&lt;li&gt;Crosire, the developer of ReShade, does not approve of Marot’s behavior&lt;/li&gt;
&lt;li&gt;Several YouTube videos were made about me (&lt;a href=&quot;https://www.youtube.com/watch?v=ORClmeq6W2A&quot;&gt;one&lt;/a&gt;, &lt;a href=&quot;https://youtu.be/Hfh3sLoDrkU&quot;&gt;two&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://gamerant.com/final-fantasy-14-gshade-mod-twitter-trend-developer-malware/&quot;&gt;This makes it onto the news&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://twitter.com/Tr3ntu/status/1622948121037742081&quot;&gt;Marot gets banned off of GitHub&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;To be clear, I didn’t do this. This’ll probably end up scaring Marot away from FOSS more, but whatever.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;GShade trends on Twitter
&lt;ul&gt;
&lt;li&gt;This bled through into the Sims and Second Life community, which is really funny to me.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Whew, what a ride.&lt;/p&gt;
&lt;h2 id=&quot;support-the-future-of-foss&quot;&gt;Support the future of FOSS&lt;/h2&gt;
&lt;p&gt;If you enjoyed this ride, you should consider supporting FOSS. Support individual developers or donate to a larger charity fighting for technological freedom. Consider donating to &lt;a href=&quot;https://sfconservancy.org/&quot;&gt;the Software Freedom Conservancy&lt;/a&gt;, &lt;a href=&quot;https://www.eff.org/&quot;&gt;the Electronic Frontier Foundation&lt;/a&gt;, or any of your choice. I’d really appreciate it.&lt;/p&gt;
&lt;p&gt;Now, back to my homework, eh? :^&lt;/p&gt;</content:encoded></item><item><title>Playing tango with GShade</title><link>https://notnite.com/blog/gshade-tango</link><guid isPermaLink="true">https://notnite.com/blog/gshade-tango</guid><description>One teenager&apos;s fight against an anti-FOSS developer</description><pubDate>Sun, 05 Feb 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;UPDATE, 02/06 @ 12:45 PM ET:&lt;/strong&gt; I’ve been banned from the GPOSERS Discord after Marot put malware inside of GShade’s installer. I’ll continue this in a part two soon.&lt;/p&gt;
&lt;p&gt;This is gonna be a fun one.&lt;/p&gt;
&lt;h2 id=&quot;the-problem&quot;&gt;The problem&lt;/h2&gt;
&lt;p&gt;GShade is a closed source &lt;a href=&quot;https://reshade.me/&quot;&gt;ReShade&lt;/a&gt; fork. Why is it closed source? Who knows!&lt;/p&gt;
&lt;p&gt;They have forced automatic updates (disabling itself until you update), annoying devs, and a userbase that’s only large because “everyone else uses it”. Their FAQ post explaining why the updates are forced is specifically designed to waste time. They have annoying, obtrusive popups. Configs randomly reset every update. It &lt;em&gt;sucks&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Their installer is a total mess that also is not open source. Of course, monkey see and monkey get idea - let’s install ReShade with the GShade shaders!&lt;/p&gt;
&lt;h2 id=&quot;enter-geezshade&quot;&gt;Enter GeezShade&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://git.n2.pm/NotNite/geezshade&quot;&gt;GeezShade&lt;/a&gt; is an open source shader/preset downloader written in Rust. It’s broken right now (i’ll get into that later), but it’s important for the story.&lt;/p&gt;
&lt;p&gt;It works by downloading .zip files from GitHub. Seriously.&lt;/p&gt;
&lt;p&gt;Almost immediately I got a tweet from a popular preset creator &lt;a href=&quot;https://web.archive.org/web/20230205195409/https://twitter.com/Xelyanne/status/1621422854926483457&quot;&gt;telling me to exclude their work from this&lt;/a&gt;. How do you exclude a preset from a program that fetches a GitHub API endpoint? I dunno.&lt;/p&gt;
&lt;p&gt;I also got accused of hosting and “distributing” shaders and presets, and even that &lt;a href=&quot;https://web.archive.org/web/20230205195629/https://twitter.com/Xelyanne/status/1621560004456112129&quot;&gt;doing this was conspiracy to commit a crime&lt;/a&gt;!. Maybe those who said that should reread what that word means.&lt;/p&gt;
&lt;h2 id=&quot;marot-responds&quot;&gt;Marot responds&lt;/h2&gt;
&lt;p&gt;Marot, a.k.a. “The CEO of GShade” (I made that title up), after fighting me in my Twitter notifs (Marot, you’re shadowbanned on Twitter, by the way) comes up with the ultimate plan - add a LICENSE.md!&lt;/p&gt;
&lt;p&gt;You can see it &lt;a href=&quot;https://web.archive.org/web/20230205195419/https://github.com/Mortalitas/GShade/blob/master/LICENSE.md&quot;&gt;here&lt;/a&gt;. Note the &lt;code&gt;automated downloading&lt;/code&gt; part. :^&lt;/p&gt;
&lt;h2 id=&quot;some-other-things-i-built&quot;&gt;Some other things I built&lt;/h2&gt;
&lt;p&gt;Here’s some other things I did to troll Marot while I got bored:&lt;/p&gt;
&lt;h3 id=&quot;gshade-tango---the-ultimate-tool&quot;&gt;GShade Tango - the ultimate tool&lt;/h3&gt;
&lt;p&gt;I also made a CLI tool that I never released anywhere that had several features - download ReShade and GShade, patch the update check out of the GShade DLL, and download shaders/presets (including pinned before they added a license!)&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;The output of the tool&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;603&quot; height=&quot;277&quot; src=&quot;https://notnite.com/_astro/cli.Dnl_8JJl_2pQR7R.webp&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;the-final-straw---gshade-patcher&quot;&gt;The final straw - GShade Patcher&lt;/h3&gt;
&lt;p&gt;I also made &lt;a href=&quot;https://notnite.com/gshade-patcher.html&quot;&gt;an online patcher tool&lt;/a&gt; to patch the update check out of GShade! It’s borked right now - I’ll fix it someday if I have the energy.&lt;/p&gt;
&lt;h2 id=&quot;marot-gets-mad&quot;&gt;Marot gets mad&lt;/h2&gt;
&lt;p&gt;I wake up one morning to a glorious version 4.1.1 of GShade. It:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;breaks GeezShade/GShade Tango&lt;/li&gt;
&lt;li&gt;breaks my patcher tool&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I’ll fix it eventually, but I’m not in the mood right now.&lt;/p&gt;
&lt;p&gt;This moves the GShade shaders and presets to a self-hosted web URL, password protected by a probably-not-legally-binding password claiming that “I indeed accessed this from the official installer”. Incredibly sad that Marot would intentionally break tools opting for a more open replacement for GShade.&lt;/p&gt;
&lt;h2 id=&quot;so-what-now&quot;&gt;So what now?&lt;/h2&gt;
&lt;p&gt;I’m getting bored of this. It was funny, though!&lt;/p&gt;
&lt;p&gt;If I were you, still trying to use GShade on ReShade, I’d give &lt;a href=&quot;https://github.com/HereInPlainSight/gshade_installer/blob/master/gshade_installer.sh&quot;&gt;this script&lt;/a&gt; a look - specifically &lt;code&gt;presetAndShaderUpdate&lt;/code&gt;’s calls to &lt;code&gt;curl&lt;/code&gt;, and the two variables at the top of that function. Have fun!&lt;/p&gt;
&lt;p&gt;If you’re reading this, Marot: you really need to pick better things to do with your time than fight 16 year olds on the Internet. I’ll be back to fight for FOSS someday, but I’ve got more important things to do like schoolwork. Thanks for making a clear stance your software practices are evil, though.&lt;/p&gt;
&lt;p&gt;This blog post was written in Vim, built with &lt;a href=&quot;https://getzola.org/&quot;&gt;Zola&lt;/a&gt;, hosted through NGINX on Debian Linux through Proxmox. How’s that for open source!&lt;/p&gt;</content:encoded></item><item><title>The feminine urge to cheat in Splatoon</title><link>https://notnite.com/blog/cheating-in-splatoon</link><guid isPermaLink="true">https://notnite.com/blog/cheating-in-splatoon</guid><description>A tale of painters, processors, and Pro Controllers</description><pubDate>Tue, 20 Sep 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;So, Splatoon 3 just came out recently. I’ve been having a lot of fun, but my favorite part about the game so far is the plaza when booting up the game.&lt;/p&gt;
&lt;p&gt;There’s a neat little feature in the Splatoon series where you can draw a 320x120 1-bit image in game, and it’ll pop up in the main plaza for other players around the world to see. There is, however, the time honoured tradition of botting your Wii U/Switch in order to automatically print out images. What’s a user-generated-content without some fun botting?&lt;/p&gt;
&lt;p&gt;I set off to hack together my own bot. The end goal? Nothing else but the beloved duck.png!&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://notnite.com/_astro/duck.BvKuzPjJ_1ImYEb.webp&quot; alt=&quot;duck.png renditioned in the Splatoon 3 message creator, using 1-bit dithering.&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;1280&quot; height=&quot;720&quot;&gt;&lt;/p&gt;
&lt;p&gt;The final product.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;However, I wouldn’t be showing you this now if it was the &lt;em&gt;only&lt;/em&gt; goal. Oh no, this is a tale of something much bigger - a tale of painters, processors, and Pro Controllers.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;ahem&lt;/em&gt; Let’s begin.&lt;/p&gt;
&lt;h2 id=&quot;it-begins-with-paint&quot;&gt;It begins with paint&lt;/h2&gt;
&lt;p&gt;So, like any other bored nerd on a Sunday morning, I started my day typing in “Splatoon image printer” into Google. I quickly found the work of &lt;a href=&quot;https://github.com/Victrid/splatplost&quot;&gt;Victrid’s splatplost&lt;/a&gt;, a Bluetooth-based printer.&lt;/p&gt;
&lt;p&gt;Splatplost worked by using the &lt;a href=&quot;https://github.com/Victrid/libnxctrl&quot;&gt;libnxctrl&lt;/a&gt; Python library, which used Bluez (the Bluetooth stack for Linux) to emulate a Pro Controller wirelessly.&lt;/p&gt;
&lt;p&gt;My main computer is a Windows machine (libnxctrl uses Bluez, so it’s Linux only), and it doesn’t even &lt;em&gt;have&lt;/em&gt; a Bluetooth adapter in it, so I’d have to work with this on my laptop.&lt;/p&gt;
&lt;p&gt;After jingling keys in front of my face for an eternity as I waited for everything to install into a virtualenv, I started it for the first time, and…&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://notnite.com/_astro/splatplost.nTH6Q_Rf_Z1rtO6a.webp&quot; alt=&quot;The splatplost GUI. Due to improper DPI scaling, all of the elements on the UI are extremely tiny.&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;3840&quot; height=&quot;2100&quot;&gt;&lt;/p&gt;
&lt;p&gt;Thus do I remember the fate of this laptop: a 4K screen with half of the GUI libraries having fucked up DPI. Whew, this is gonna be a bit…&lt;/p&gt;
&lt;p&gt;So, I clicked the “Start Pairing” button, and… it did nothing. Why? There’s no logs, so no way to tell!&lt;/p&gt;
&lt;p&gt;After bashing my head against GitHub issues for a while, I realized that splatplost’s GUI is relatively new. Before Splatoon 3, it was just a CLI app split into multiple Python scripts.&lt;/p&gt;
&lt;p&gt;I decided to clone the repo myself, go back to an older period in Git history (before the GUI), and manually applied the required fixes for Splatoon 3 myself. After doing this, I was greeted with an error in my console:&lt;/p&gt;
&lt;p&gt;…drum roll, please…&lt;/p&gt;
&lt;p&gt;…hciconfig was required. Here’s a fun fact about hciconfig: It’s so deprecated, not on the official Arch repositories (had to download it through the AUR), &lt;em&gt;and&lt;/em&gt; the AUR description literally says “deprecated hciconfig tool from bluez”.&lt;/p&gt;
&lt;p&gt;And there was no disclaimer or warning from this. Thankfully, I left a comment on GitHub, and the developer added more logging. Hopefully more Arch-brained users can benefit from this in the future.&lt;/p&gt;
&lt;p&gt;With that headache over, Splatplost now worked, but I noticed it was working slowly. See, the thing is, Splatplost has a nice pathing system to try and find the best way to draw the images. However, literally going &lt;em&gt;line-by-line&lt;/em&gt; would have been faster for this image, and Splatplost wasn’t optimized enough to know this.&lt;/p&gt;
&lt;p&gt;Because I didn’t want to wait here for a projected 2 to 3 hours, I looked at my other options. What now?&lt;/p&gt;
&lt;h2 id=&quot;where-were-going-we-dont-need-pathing&quot;&gt;Where we’re going, we don’t need pathing&lt;/h2&gt;
&lt;p&gt;See, the beauty of Splatplost is that the controller part is &lt;em&gt;just&lt;/em&gt; a Python library. So we can do it ourselves!&lt;/p&gt;
&lt;p&gt;I don’t really write Python often, and when I do, it’s messy quick hacks like these. And I’m &lt;em&gt;perfect&lt;/em&gt; at quick hacks like these.&lt;/p&gt;
&lt;p&gt;So, I wrote my own painter in about half an hour, using Splatplost as a reference. You can see it &lt;a href=&quot;https://haste.soulja-boy-told.me/esebunatap.py&quot;&gt;here&lt;/a&gt; if you’d like, but it’s not very fast (or creative).&lt;/p&gt;
&lt;p&gt;I was able to paint two images, but I noticed something wrong with it. Due to the nature of Bluetooth, I couldn’t crank the speed too high or else it would desync and mess up. Even worse, it still seemed to make a mistake every once in a while!&lt;/p&gt;
&lt;p&gt;I ended up tweeting about it, and my good friend &lt;a href=&quot;https://aly.fish/&quot;&gt;Aly&lt;/a&gt; chimed in:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;have you considered just doing it with tas-script or sys-script? I made my Mario Maker 2 painter with nx-TAS (now obselete) but the format is still supported by tas-script&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This was an interesting avenue. I have a homebrewed Switch - I actually have two of them, because I bought one specifically for homebrew back before emuMMC existed.&lt;/p&gt;
&lt;p&gt;However, that homebrewed Switch doesn’t own Splatoon - and given the amount of, uh, “non-Nintendo-friendly NSPs” that were installed, I didn’t want to connect it to my Nintendo account.&lt;/p&gt;
&lt;p&gt;That’s when Aly suggested &lt;a href=&quot;https://github.com/MonsterDruide1/ArduinoTAS&quot;&gt;MonsterDruide’s ArduinoTAS&lt;/a&gt;, which begins the start of the hardware rabbithole.&lt;/p&gt;
&lt;h2 id=&quot;creating-tool-assisted-paintings&quot;&gt;Creating tool assisted paintings&lt;/h2&gt;
&lt;p&gt;ArduinoTAS is a fork of &lt;a href=&quot;https://github.com/wchill/SwitchInputEmulator&quot;&gt;wchill’s SwitchInputEmulator&lt;/a&gt;, with a bit of Python script glue to support running a TAS script. The idea is to simulate a Pro Controller over USB using an Arduino.&lt;/p&gt;
&lt;p&gt;If you’re not familiar with what a “TAS” is - it means “tool assisted speedrun”. This is specifically designed to play back a list of inputs in a specific order and timing, designed for speedruns performed by a computer to display the theoretical perfect speedrun.&lt;/p&gt;
&lt;p&gt;The idea is simple - set up ArduinoTAS, write a program that takes an image and produces a TAS script, and then play that script on the Arduino. The real challenge is setting up ArduinoTAS.&lt;/p&gt;
&lt;p&gt;ArduinoTAS isn’t actively maintained, so much so that the images in the README were dead. With little knowledge of how an Arduino really worked, I gathered one from the depths of my technology bin, and set out to understand the README.&lt;/p&gt;
&lt;p&gt;ArduinoTAS had two interesting components to it: it required a USB to UART adapter (to send scripts over - the USB port for the Arduino was being use to emulate a Pro Controller), and it required a HDMI to VGA adapter. You would wire up the VSync pin from the VGA adapter to the Arduino, so it has an accurate reading of when each frame starts. That’s some &lt;em&gt;really&lt;/em&gt; clever black magic.&lt;/p&gt;
&lt;p&gt;After a long while of troubleshooting, I eventually figured out what cables to connect and where. I ended up using a separate Arduino instead of a USB to UART adapter, because I had one lying around and I didn’t want to wait 2 days for the package to arrive.&lt;/p&gt;
&lt;p&gt;I didn’t have a soldering iron upstairs, so I just kind of touched the wire to the right pin using the power of friendship:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://notnite.com/_astro/worlds-worst-cable.DpVqHjuP_Z2bK6fM.webp&quot; alt=&quot;A wire improperly connected to an Arduino using only the power of friendship.&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;3024&quot; height=&quot;4032&quot;&gt;&lt;/p&gt;
&lt;p&gt;Check this cable out!&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Now, all I needed to do was write a program to print out the images. And then I had an idea.&lt;/p&gt;
&lt;p&gt;“What if you could emulate a controller and use a keyboard and mouse to play Splatoon?”&lt;/p&gt;
&lt;h2 id=&quot;cheating-with-the-power-of-python&quot;&gt;Cheating with the power of Python&lt;/h2&gt;
&lt;p&gt;In a haste of motivation, I quickly copied the &lt;code&gt;clientTAS.py&lt;/code&gt; from the ArduinoTAS repository, and installed pygame into my virtual environment. It was time to make something &lt;em&gt;sinister&lt;/em&gt;. By the way, at this point, I had finally set up my dual boot and was writing this code on my desktop.&lt;/p&gt;
&lt;p&gt;Using the power of 10 if/else statements, I hacked together a proof of concept:&lt;/p&gt;
&lt;div class=&quot;video&quot; data-astro-cid-7qzxku2k=&quot;&quot;&gt; &lt;video controls=&quot;&quot; src=&quot;https://notnite.com/blog/res/cheating-in-splatoon/mnk.mp4&quot; data-astro-cid-7qzxku2k=&quot;&quot;&gt;&lt;/video&gt; &lt;/div&gt;
&lt;p&gt;Check that out! Nifty, huh?&lt;/p&gt;
&lt;p&gt;Of course, I’d instantly get banned if I queued into a public lobby with this. Not that I could, anyways - inputs were being dropped and the camera wasn’t precise.&lt;/p&gt;
&lt;p&gt;ArduinoTAS, though very useful for painting, wasn’t very useful for cheating in Splatoon. Instead, I’d head upstream for that!&lt;/p&gt;
&lt;h2 id=&quot;swimming-up-the-stream&quot;&gt;Swimming up the stream&lt;/h2&gt;
&lt;p&gt;As said before, ArduinoTAS is just a fork of &lt;a href=&quot;https://github.com/wchill/SwitchInputEmulator&quot;&gt;SwitchInputEmulator&lt;/a&gt;, which is probably more suited for the task of emulating controller input. Let’s take a crack!&lt;/p&gt;
&lt;p&gt;After a short flash later, it seems to work, but we need to get a script going. There exists a &lt;code&gt;InputServer&lt;/code&gt; we can use in the repository.&lt;/p&gt;
&lt;p&gt;It’s a C# server, targeting .NET 4.7.1. Updating the dependencies, changing the target framework, and changing the serial path is enough to get it building on Linux.&lt;/p&gt;
&lt;p&gt;There seems to exist some sort of WebSocket functionality to control it, but it seems geared around Twitch, which I’m definitely not doing.&lt;/p&gt;
&lt;p&gt;There also exists a “MultiInput”, which is a QT app. I kid you not, they don’t ship a Makefile or anything useful - they ship &lt;strong&gt;a Linux binary precompiled in the source tree&lt;/strong&gt;. Jesus Christ.&lt;/p&gt;
&lt;p&gt;I mean, it &lt;em&gt;runs&lt;/em&gt;, but all the inputs are messed up and I have no idea how to build it. Given that’s both ways of interacting crossed out, I seeked to write my own client in Rust, but it ended up just stalling and I couldn’t figure out why.&lt;/p&gt;
&lt;p&gt;Thus, did I stumble across the final part of this journey:&lt;/p&gt;
&lt;h2 id=&quot;the-bluetooth-ouroboros&quot;&gt;The Bluetooth ouroboros&lt;/h2&gt;
&lt;p&gt;You know, I originally moved to the Arduino, because Bluetooth was unstable. And now that my goal isn’t precise printing, and just controlling the game with a keyboard &amp;amp; mouse, Bluetooth looked like a good option!&lt;/p&gt;
&lt;p&gt;One trip to Micro Center later, my PC now has Bluetooth support. I stumbled upon &lt;a href=&quot;https://github.com/Brikwerk/nxbt&quot;&gt;nxbt&lt;/a&gt;, another Python library using Bluez to emulate a Pro Controller - this time, with an already working web app!&lt;/p&gt;
&lt;p&gt;Installing this was a &lt;em&gt;nightmare&lt;/em&gt;. The dependencies were messed up and broken on Python 3.10, so I had to downgrade some things manually. If I wasn’t using a virtual environment I might have cried.&lt;/p&gt;
&lt;p&gt;The web app, sadly, did not have any rebinding features. That’s okay - it’s a library! Following in the footsteps of ArduinoTAS, I hacked together another pygame app. And it literally looked the exact same, so much so that I won’t even bother to include the (80 megabyte) video of it!&lt;/p&gt;
&lt;p&gt;The mouse was more choppier, strangely, but the inputs felt a bit more responsive (which is funny, for having more latency). Maybe I can fix this with a rewrite in not-pygame.&lt;/p&gt;
&lt;p&gt;I also considered trying to emulate a controller with keyboard/mouse inputs, and just use the web app, but I couldn’t find anything that did that on Linux.&lt;/p&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;That sure was an ADHD spiral if I’ve ever seen one.&lt;/p&gt;
&lt;p&gt;If you’re looking to print out images - use Splatplost or &lt;a href=&quot;https://github.com/shinyquagsire23/Switch-Fightstick&quot;&gt;Shiny Quagsire’s Arduino printer&lt;/a&gt; - I ended up using the latter after I got bored of making my own, and it seems to work somewhat well. If you’re looking to cheat in Splatoon - please don’t. This was purely for my curiosity and I don’t plan to use this in online matches. Pls no ban Nintendo.&lt;/p&gt;
&lt;p&gt;Shoutouts to all the developers making all the code referenced in this post - you’re powering my skid dreams. Thank you!&lt;/p&gt;</content:encoded></item><item><title>FFXIV Explained - The Login Process</title><link>https://notnite.com/blog/ffxiv-login-process</link><guid isPermaLink="true">https://notnite.com/blog/ffxiv-login-process</guid><description>Explaining the FFXIV login process based on research done by XIVLauncher</description><pubDate>Thu, 28 Jul 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;For a while now, I’ve wanted to write up something interesting about FFXIV’s systems. I thought it would be a fun exercise and entertaining to write about how the login flow works, so I’m doing it!&lt;/p&gt;
&lt;p&gt;This blog post will be aimed at semi-technical people familiar with some basic HTTP concepts, but I’ll be linking to Wikipedia pages for some technical terms, so anyone can understand the post if they wish.&lt;/p&gt;
&lt;p&gt;If you know C#, feel free to follow along &lt;a href=&quot;https://github.com/goatcorp/FFXIVQuickLauncher/blob/master/src/XIVLauncher/Windows/ViewModel/MainWindowViewModel.cs&quot;&gt;in the XIVLauncher source code&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;boot-versions&quot;&gt;Boot Versions&lt;/h2&gt;
&lt;p&gt;When updating the game, there are two types of updates: &lt;code&gt;boot&lt;/code&gt; and &lt;code&gt;game&lt;/code&gt;. Boot is the launcher, which requires no authentication, and game is the game files - you need to log in successfully to get the patch list. Fun fact: The files themselves aren’t authenticated - if you know the URL, you can download the patch, even without a service account.&lt;/p&gt;
&lt;p&gt;We make an HTTP request to &lt;code&gt;http://patch-bootver.ffxiv.com/http/win32/ffxivneo_release_boot/game_ver/&lt;/code&gt;, where &lt;code&gt;game_ver&lt;/code&gt; is either the version file on disk, or the base game version (&lt;code&gt;2012.01.01.0000.0000&lt;/code&gt;). We also add a &lt;a href=&quot;https://en.wikipedia.org/wiki/Query_string&quot;&gt;query string&lt;/a&gt; of the current time, formatted in &lt;code&gt;yyyy-MM-dd-HH-mm&lt;/code&gt;, where the last character is a zero. This seems to not be required - it’s probably for caching.&lt;/p&gt;
&lt;p&gt;You can actually make this request yourself to see what it looks like:&lt;/p&gt;
&lt;pre class=&quot;language-sh&quot; data-language=&quot;sh&quot;&gt;&lt;code class=&quot;language-sh&quot;&gt;&lt;span class=&quot;token function&quot;&gt;curl&lt;/span&gt; http://patch-bootver.ffxiv.com/http/win32/ffxivneo_release_boot/2012.01.01.0000.0000/ &lt;span class=&quot;token parameter variable&quot;&gt;-H&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;User-Agent: FFXIV PATCH CLIENT&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If the request returns a 204 No Content, that means we’re up to date - else, we should parse this patch list. Each line is a patch, with parameters separated by &lt;code&gt;\t&lt;/code&gt;. You can see the patch length (first argument), version ID, and URL to the patch. The client should then download and install these patch files, but that’s out of scope for this post.&lt;/p&gt;
&lt;h2 id=&quot;steam-tickets&quot;&gt;Steam Tickets&lt;/h2&gt;
&lt;p&gt;If the user is using a Steam service account, we’ll need a ticket from the Steamworks API. Let’s break down what happens in the code here.&lt;/p&gt;
&lt;p&gt;We initialize the Steamworks API, using the free trial app ID if chosen in the XIVLauncher settings, and get the ticket:&lt;/p&gt;
&lt;pre class=&quot;language-cs&quot; data-language=&quot;cs&quot;&gt;&lt;code class=&quot;language-cs&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token return-type class-name&quot;&gt;Task&lt;span class=&quot;token punctuation&quot;&gt;&amp;#x3C;&lt;/span&gt;Ticket&lt;span class=&quot;token punctuation&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;Get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;ISteam&lt;/span&gt; steam&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token class-name&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt;&lt;/span&gt; ticketBytes &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; steam&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;GetAuthSessionTicketAsync&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;ConfigureAwait&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;ticketBytes &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;EncryptAuthSessionTicket&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;ticketBytes&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; steam&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;GetServerRealTime&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Square Enix has decided to utilize their MASSIVE BRAIN once again and encrypt the ticket, for seemingly no good reason. It can’t be that bad though, right? Let’s see what’s in &lt;code&gt;EncryptAuthSessionTicket&lt;/code&gt;…&lt;/p&gt;
&lt;pre class=&quot;language-cs&quot; data-language=&quot;cs&quot;&gt;&lt;code class=&quot;language-cs&quot;&gt;time &lt;span class=&quot;token operator&quot;&gt;-=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
time &lt;span class=&quot;token operator&quot;&gt;-=&lt;/span&gt; time &lt;span class=&quot;token operator&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;60&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// Time should be rounded to nearest minute.&lt;/span&gt;

&lt;span class=&quot;token class-name&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt;&lt;/span&gt; blowfishKey &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token interpolation-string&quot;&gt;&lt;span class=&quot;token string&quot;&gt;$&quot;&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token expression language-csharp&quot;&gt;time&lt;/span&gt;&lt;span class=&quot;token format-string&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;x08&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;#un@e=x&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Ah… truly a beauty to see. Square Enix loves abusing &lt;a href=&quot;https://en.wikipedia.org/wiki/Blowfish_(cipher)&quot;&gt;Blowfish&lt;/a&gt; in literally every scenario possible. Fun fact: In FFXIV’s netcode, they use a broken implementation of Blowfish! We refer to this as “Brokefish” in the community.&lt;/p&gt;
&lt;p&gt;Next, we do some strange things to the ticket:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;We turn it into a string, remove any dashes from the string, and turn it back to bytes. Why? Who knows!&lt;/li&gt;
&lt;li&gt;We create a byte array 1 byte longer than the ticket, filling the array with the ticket, and zeroing out the last byte.&lt;/li&gt;
&lt;li&gt;We create a sum by iterating through the ticket and adding each byte to a ushort.&lt;/li&gt;
&lt;li&gt;We create a &lt;a href=&quot;https://docs.microsoft.com/en-us/dotnet/api/system.io.binarywriter?view=net-6.0&quot;&gt;BinaryWriter&lt;/a&gt; with a &lt;a href=&quot;https://docs.microsoft.com/en-us/dotnet/api/system.io.memorystream?view=net-6.0&quot;&gt;MemoryStream&lt;/a&gt;, write the ticket sum to it, and then write the ticket data to it.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We cast the ticket sum to a short, and use the time to create an instance of CrtRand (an implementation of the Windows libc random number generator):&lt;/p&gt;
&lt;pre class=&quot;language-cs&quot; data-language=&quot;cs&quot;&gt;&lt;code class=&quot;language-cs&quot;&gt;&lt;span class=&quot;token class-name&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt;&lt;/span&gt; castTicketSum &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;unchecked&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;short&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;ticketSum&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token class-name&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt;&lt;/span&gt; seed &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; time &lt;span class=&quot;token operator&quot;&gt;^&lt;/span&gt; castTicketSum&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token class-name&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt;&lt;/span&gt; rand &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token constructor-invocation class-name&quot;&gt;CrtRand&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;uint&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;seed&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We’re going to have to use Square Enix’s “fucked garbage alphabet” in this next step, to create “garbage”. If you’re curious, their fucked garbage alphabet is &lt;code&gt;0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;We then create garbage:&lt;/p&gt;
&lt;pre class=&quot;language-cs&quot; data-language=&quot;cs&quot;&gt;&lt;code class=&quot;language-cs&quot;&gt;&lt;span class=&quot;token class-name&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt;&lt;/span&gt; numRandomBytes &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;ulong&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;rawTicket&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Length &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;9&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;#x26;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0xFFFFFFFFFFFFFFF8&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;ulong&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;rawTicket&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Length&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token class-name&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt;&lt;/span&gt; garbage &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token constructor-invocation class-name&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;byte&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;numRandomBytes&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token class-name&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;uint&lt;/span&gt;&lt;/span&gt; fuckedSum &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; BitConverter&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;ToUInt32&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;memorySteam&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;ToArray&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt;&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0u&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;&amp;#x3C;&lt;/span&gt; numRandomBytes&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; i&lt;span class=&quot;token operator&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token class-name&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt;&lt;/span&gt; randChar &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; FUCKED_GARBAGE_ALPHABET&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;fuckedSum &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; rand&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;Next&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;#x26;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0x3F&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    garbage&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;i&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;byte&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;randChar&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    fuckedSum &lt;span class=&quot;token operator&quot;&gt;+=&lt;/span&gt; randChar&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We then write the garbage, and overwrite the first byte with the &lt;code&gt;fuckedSum&lt;/code&gt;. Now we’re going to encrypt it!&lt;/p&gt;
&lt;p&gt;We swap the first and second bytes, and use the Blowfish key we generated earlier to create an ECB Blowfish cipher. We encrypt the bytes, and - get ready for it - turn it into a custom variant of Base64!&lt;/p&gt;
&lt;p&gt;This Base64 is basically the same as the regular implementation, but the following characters are swapped:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;+&lt;/code&gt; to &lt;code&gt;-&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/&lt;/code&gt; to &lt;code&gt;_&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;=&lt;/code&gt; to &lt;code&gt;*&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We then chunk the Base64 string into chunks of 300 bytes, and join these chunks with a comma. And that’s how you make a fucked up Steam ticket, Square Enix style!&lt;/p&gt;
&lt;h2 id=&quot;checking-gate-status&quot;&gt;Checking Gate Status&lt;/h2&gt;
&lt;p&gt;We make a request to &lt;code&gt;https://frontier.ffxiv.com/worldStatus/gate_status.json&lt;/code&gt;, which just returns JSON telling us if the “gates are open” - if we should continue attempting a login.&lt;/p&gt;
&lt;h2 id=&quot;performing-an-oauth-login&quot;&gt;Performing an OAuth Login&lt;/h2&gt;
&lt;p&gt;We’re gonna need to use the system called OAuth (but not really like OAuth) to log in now. We’re gonna make a request to &lt;code&gt;https://ffxiv-login.square-enix.com/oauth/ffxivarr/login/top&lt;/code&gt; with the following query string:&lt;/p&gt;

































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;key&lt;/th&gt;&lt;th&gt;value&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;lng&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;en&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;rgn&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;3&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;isft&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;1&lt;/code&gt; or &lt;code&gt;0&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;cssmode&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;1&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;isnew&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;1&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;launchver&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;3&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;If we’re on Steam, we add the following values:&lt;/p&gt;





















&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;key&lt;/th&gt;&lt;th&gt;value&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;issteam&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;1&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;session_ticket&lt;/code&gt;&lt;/td&gt;&lt;td&gt;(the ticket)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;ticket_size&lt;/code&gt;&lt;/td&gt;&lt;td&gt;(ticket length)&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;We then make a request to the formed URL, and the response is: …HTML?&lt;/p&gt;
&lt;p&gt;Yep, that’s right! There’s no API in this town, the launcher is literally just HTML being served by an Apache server. It even comes with raw, commented-with-Japanese-text JavaScript, baked into the website!&lt;/p&gt;
&lt;p&gt;Using a &lt;a href=&quot;https://en.wikipedia.org/wiki/Regular_expression&quot;&gt;regex&lt;/a&gt; is faster than parsing HTML, so we use it to grab a &lt;em&gt;mystical value&lt;/em&gt; called &lt;code&gt;_STORED_&lt;/code&gt;. We’ll used STORED later on to exchange a session ID, which we can then use to start the game.&lt;/p&gt;
&lt;p&gt;If we’re on Steam, we also use another regex to grab the username of the Steam account associated with this Square Enix account. We can use this to double check we’re on the right account.&lt;/p&gt;
&lt;p&gt;We’re then gonna make a POST request to &lt;code&gt;https://ffxiv-login.square-enix.com/oauth/ffxivarr/login/login.send&lt;/code&gt;, with a &lt;code&gt;application/x-www-form-urlencoded&lt;/code&gt; form:&lt;/p&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;key&lt;/th&gt;&lt;th&gt;value&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;_STORED_&lt;/code&gt;&lt;/td&gt;&lt;td&gt;the value we obtained earlier&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;sqexid&lt;/code&gt;&lt;/td&gt;&lt;td&gt;your username&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;password&lt;/code&gt;&lt;/td&gt;&lt;td&gt;your password&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;otppw&lt;/code&gt;&lt;/td&gt;&lt;td&gt;your OTP, if any&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;We regex the return value of this to get the following information:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Your session ID&lt;/li&gt;
&lt;li&gt;Region of the account&lt;/li&gt;
&lt;li&gt;If you’ve accepted the terms of service&lt;/li&gt;
&lt;li&gt;If the account is “playable” (e.g. subscription paid up)&lt;/li&gt;
&lt;li&gt;The expansions owned by this account&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;registering-a-session&quot;&gt;Registering a session&lt;/h2&gt;
&lt;p&gt;We now have a session ID! We can use this to register a session.&lt;/p&gt;
&lt;p&gt;Before this, we do the minimum to ensure our game isn’t corrupted by matching the &lt;code&gt;.ver&lt;/code&gt; and &lt;code&gt;.bck&lt;/code&gt; files, to make sure the version files aren’t corrupted. If the version files are corrupted, it’s a 90% chance the game is, too.&lt;/p&gt;
&lt;p&gt;We make another POST request to &lt;code&gt;https://patch-gamever.ffxiv.com/http/win32/ffxivneo_release_game/game_ver/session_id&lt;/code&gt;, where &lt;code&gt;game_ver&lt;/code&gt; is the game version (or the fallback 2012.01.01 one), and &lt;code&gt;session_id&lt;/code&gt; is our session ID.&lt;/p&gt;
&lt;p&gt;We’re going to need to send along a version report. It looks like this:&lt;/p&gt;
&lt;pre class=&quot;language-text&quot; data-language=&quot;text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;2022.03.25.0000.0001=(BOOT HASH INFO)
ex1 2022.05.26.0000.0000
ex2 2022.05.26.0000.0000
ex3 2022.05.26.0000.0000
ex4 2022.05.27.0000.0000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The hash info is an array of strings, joined by &lt;code&gt;,&lt;/code&gt;. Each entry is made up of information regarding a file, to make sure none of the launcher’s files have been tampered with. Each entry looks like &lt;code&gt;file_name/file_length/file_hash&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;This also will return patch info, with a slightly different structure than to boot. If we need to update the game, we do it here (again, out of scope). We also get an &lt;code&gt;X-Patch-Unique-Id&lt;/code&gt; header, which we get to keep for later.&lt;/p&gt;
&lt;p&gt;If we get a 409 Conflict, that means we’re in the “everything is fucked” state. This means the files have been tampered with, most likely by a third party, and that we should panic! D: A 410 Gone means that one of the versions we have isn’t being serviced by the launcher anymore.&lt;/p&gt;
&lt;h2 id=&quot;starting-the-game&quot;&gt;Starting the game&lt;/h2&gt;
&lt;p&gt;Finally! We can start &lt;code&gt;ffxiv_dx11.exe&lt;/code&gt; with the following arguments, in &lt;code&gt;key=value&lt;/code&gt; form:&lt;/p&gt;









































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;key&lt;/th&gt;&lt;th&gt;value&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;DEV.DataPathType&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;1&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;DEV.MaxEntitledExpansionID&lt;/code&gt;&lt;/td&gt;&lt;td&gt;value from oauth login&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;DEV.TestSID&lt;/code&gt;&lt;/td&gt;&lt;td&gt;session ID&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;DEV.UseSqPack&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;1&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;SYS.Region&lt;/code&gt;&lt;/td&gt;&lt;td&gt;value from oauth login&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;language&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;0&lt;/code&gt;-&lt;code&gt;3&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;resetConfig&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;0&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;ver&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;.ver&lt;/code&gt; file from game install&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;I find it quite funny the “Test SID” is the legitimate session ID used to start the game. Ah, Square Enix…&lt;/p&gt;
&lt;p&gt;We also add &lt;code&gt;IsSteam=1&lt;/code&gt; and set the environment variable &lt;code&gt;IS_FFXIV_LAUNCH_FROM_STEAM&lt;/code&gt; to &lt;code&gt;1&lt;/code&gt; when launching through Steam.&lt;/p&gt;
&lt;p&gt;The game also (optionally) accepts encrypted arguments, based on the current time. It’s a bit too much to explain, so see &lt;a href=&quot;https://github.com/goatcorp/FFXIVQuickLauncher/blob/master/src/XIVLauncher.Common/Encryption/ArgumentBuilder.cs&quot;&gt;here&lt;/a&gt; if you’re interested.&lt;/p&gt;
&lt;h2 id=&quot;closing-thoughts&quot;&gt;Closing thoughts&lt;/h2&gt;
&lt;p&gt;Welp, that was a waste of a few hours. Maybe in a future post I’ll cover the lobby server, based on research from &lt;a href=&quot;https://github.com/SapphireServer/Sapphire&quot;&gt;Sapphire&lt;/a&gt;…&lt;/p&gt;
&lt;p&gt;I hope you enjoyed reading this! Sorry if I got a bit rambly at times.&lt;/p&gt;</content:encoded></item></channel></rss>