<?xml version='1.0' encoding='UTF-8'?>
<rss xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" version="2.0">
  <channel>
    <title>Layla's Blog</title>
    <link>https://layla.gg/blog</link>
    <description>Thoughts on software development, productivity, and life.</description>
    <docs>http://www.rssboard.org/rss-specification</docs>
    <generator>python-feedgen</generator>
    <image>
      <url>https://layla.gg/static/favicon.png</url>
      <title>Layla's Blog</title>
      <link>https://layla.gg/blog</link>
    </image>
    <language>en</language>
    <lastBuildDate>Tue, 17 Feb 2026 19:17:40 +0000</lastBuildDate>
    <item>
      <title>How TetraForce runs Godot on AWS</title>
      <link>https://layla.gg/blog/2020_11_03_tetraforce</link>
      <description>A deep dive into the infrastructure making TetraForce work</description>
      <content:encoded>&lt;p&gt;&lt;img alt="TetraForce Screenshot" src="/static/blog/2020-11-03/tetraforce_screenshot_1.png" /&gt;&lt;/p&gt;
&lt;p&gt;TetraForce is an open-source multiplayer action-adventure RPG inspired by the popular Zelda game, Link's Awakening. It uses Godot’s built-in UDP networking library and scripting language for most of the game’s logic.&lt;/p&gt;
&lt;p&gt;The main developers managing the project are &lt;a href="https://twitter.com/_fornclake"&gt;fornclake&lt;/a&gt; and &lt;a href="https://twitter.com/TheRetroDragon"&gt;TheRetroDragon&lt;/a&gt;. Back in July, I got in contact with them to discuss putting TetraForce into the cloud! Within a week, TetraForce was running on AWS. Since then, there have been many gradual improvements to get the project where it is today.&lt;/p&gt;
&lt;p&gt;For a summary of this project, the Amazon Elastic Container Service cluster manages and runs TetraForce’s containers using a serverless API. Initially, our cluster was hosted in an auto-scaling group of EC2 instances (Virtual Machines). Later, it was moved to &lt;a href="https://aws.amazon.com/fargate/"&gt;AWS Fargate&lt;/a&gt; (Serverless Container Platform) to simplify scaling and reduce costs during periods of low utilization.&lt;/p&gt;
&lt;p&gt;There is a Serverless REST API using Lambda for creating and joining rooms, which sends and sets server information, such as name and ECS task ID, in a DynamoDB table. When a server closes, an additional lambda removes it from the DynamoDB table.&lt;/p&gt;
&lt;p&gt;Now let’s jump into some of the details.&lt;/p&gt;
&lt;p&gt;&lt;img alt="AWS Diagram displaying which services are used within TetraForce’s cloud infrastructure" src="/static/blog/2020-11-03/tetraforce_infra_diagram.png" /&gt;&lt;/p&gt;
&lt;h2 id="dockerization"&gt;Dockerization&lt;/h2&gt;
&lt;p&gt;Godot being lightweight, is easy to Dockerize. For the TetraForce Docker image, it just installs dependencies, the Godot server runtime, and the TetraForce’s pckfile. It can easily be extended to add additional pck files in the future for mods or expansions.&lt;/p&gt;
&lt;p&gt;As of when this post was written, this is our working Dockerfile:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-dockerfile"&gt;FROM centos:centos8

RUN yum install -y wget unzip libXcursor openssl openssl-libs libXinerama libXrandr-devel libXi alsa-lib pulseaudio-libs mesa-libGL

ENV GODOT_VERSION &amp;quot;3.2.2&amp;quot;

# Install Godot Server
RUN wget -q https://downloads.tuxfamily.org/godotengine/${GODOT_VERSION}/Godot_v${GODOT_VERSION}-stable_linux_headless.64.zip \
    &amp;amp;&amp;amp; unzip Godot_v${GODOT_VERSION}-stable_linux_headless.64.zip \
    &amp;amp;&amp;amp; mv Godot_v${GODOT_VERSION}-stable_linux_headless.64 /usr/local/bin/godot \
    &amp;amp;&amp;amp; chmod +x /usr/local/bin/godot

# Create Runtime User
RUN useradd -d /tetra tetra


# Add pck file
ADD build/TetraForce.pck /tetra/TetraForce.pck

CMD /usr/local/bin/godot --main-pack /tetra/TetraForce.pck --empty-server-timeout=300
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;CMD&lt;/code&gt; for the image is to run the Godot server runtime using the TetraForce &lt;code&gt;pck&lt;/code&gt; file. You can notice in the run command we also pass a &lt;code&gt;--empty-server-timeout=300&lt;/code&gt;; this is for our containers to close servers that have no active players automatically.
Server Management&lt;/p&gt;
&lt;p&gt;Each game room runs as a task in ECS. Amazon assigns those tasks a public IP. The client receives those IPs by interacting with TetraForce’s API.&lt;/p&gt;
&lt;h2 id="the-rest-api"&gt;The REST API&lt;/h2&gt;
&lt;p&gt;The client can get room information and create new rooms by making HTTPS requests to the REST API. The REST API uses API Gateway to route requests to a Lambda functions.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;/create_server&lt;/code&gt; endpoint takes in an optional parameter of a name. If it does not have a name, it will randomly generate one. From there, it will spin up a new ECS task tagged with the room’s name and add a new entry into the DynamoDB table. The Lambda function will return whether it was a success and the name of the room created.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;/get_servers&lt;/code&gt; endpoint takes in an optional parameter of a room name and page. If it’s passed a name, it will only do a lookup for the given room. Otherwise, it will return a list of room information. The room information includes the room name, public IP, and port. It’s also modular to all for additional values in the future, such as version.&lt;/p&gt;
&lt;h2 id="cloudwatch-events"&gt;CloudWatch Events&lt;/h2&gt;
&lt;p&gt;Server cleanup is managed by an additional Lambda function that is triggered by a CloudWatch Event Rule:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-json"&gt;{
  &amp;quot;source&amp;quot;: [&amp;quot;aws.ecs&amp;quot;],
  &amp;quot;detail-type&amp;quot;: [ &amp;quot;ECS Task State Change&amp;quot; ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This event will trigger the Lambda anytime an ECS Task in the cluster enters a new state. That leaves the responsibility of verifying the task’s state and then removing the room from the DynamoDB table if it’s no longer running to the Lambda function.&lt;/p&gt;
&lt;h2 id="client-integration"&gt;Client Integration&lt;/h2&gt;
&lt;p&gt;Using Godot’s &lt;code&gt;HTTPRequest&lt;/code&gt; Node, the client can hit the REST API hosted on AWS to request server information or create a new lobby.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-gdscript"&gt;export(String) var api_endpoint = &amp;quot;api.tetraforce.io&amp;quot;

var _http_client : HTTPRequest# Asynchronous coroutine.
# Requests API for data from a specific server
# Returns: {&amp;quot;message&amp;quot;: [MESSAGE], &amp;quot;data&amp;quot; : [DATA] }
func get_server(lobby : String) -&amp;gt; Dictionary:
```gdscript
_http_client.request(&amp;quot;https://&amp;quot; + api_endpoint + &amp;quot;/get_servers?server=&amp;quot; + str(lobby), [], true, HTTPClient.METHOD_GET)
var result = yield(_http_client, &amp;quot;request_completed&amp;quot;)
if len(result) &amp;gt; 3 and result[1] == 200:
    var json : JSONParseResult = JSON.parse(result[3].get_string_from_utf8())
    if json.error:
        return _build_error_message(json.error_string)
    return json.result

return _build_error_message(&amp;quot;Request failed!&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After that, the client will parse the returned JSON object into a dictionary. That dictionary will include the public server IP and Port, which is passed into the connection method when the player attempts to connect to the game server.&lt;/p&gt;
&lt;h2 id="whats-next"&gt;What’s next?&lt;/h2&gt;
&lt;p&gt;The team is currently moving forward towards building a full demo of TetraForce within the next few months. The demo will include a handful of zones, a dungeon, and an eventual boss battle.&lt;/p&gt;
&lt;p&gt;The team is looking to include skins in their game as Patreon rewards on the infrastructure side, so a user identity system is here on the horizon.&lt;/p&gt;
&lt;p&gt;If you have any questions about anything, feel free to reach out to me on Twitter.&lt;/p&gt;
&lt;h2 id="related-links"&gt;Related Links&lt;/h2&gt;
&lt;p&gt;TetraForce Discord: &lt;a href="https://discord.gg/cxTBVCZ"&gt;https://discord.gg/cxTBVCZ&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;TetraForce Repository: &lt;a href="https://github.com/loudsmilestudios/tetraforce"&gt;https://github.com/loudsmilestudios/tetraforce&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Infrastructure Repository: &lt;a href="https://github.com/loudsmilestudios/tetraforce-infrastructure"&gt;https://github.com/loudsmilestudios/tetraforce-infrastructure&lt;/a&gt;&lt;/p&gt;</content:encoded>
      <guid isPermaLink="false">/blog/2020_11_03_tetraforce</guid>
      <pubDate>Tue, 03 Nov 2020 00:00:00 +0053</pubDate>
    </item>
    <item>
      <title>Generating Voxel Worlds</title>
      <link>https://layla.gg/blog/2024-07-22_Generating_Voxel_Worlds</link>
      <description>Looking into Voxel Generation in Godot</description>
      <content:encoded>&lt;p&gt;I’m currently working on prototyping a game project in my personal time that involves a lot of procedural generation and simulated systems. To achieve this, I’ve been investigating voxel world generation would assist me in that goal.&lt;/p&gt;
&lt;p&gt;There is a fantastic Godot module out there simply called Voxel Tools for Godot, which provides all the essentials for rendering and managing voxel data in real-time.&lt;/p&gt;
&lt;p&gt;Initially while just testing around the module, I was able to create a some basic generation from using he built-in generators within the module and quickly was able to get something up and running.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Basic generation via the VoxelGeneratorWaves class" src="/static/blog/2024-07-22/basic_generation.png" /&gt;&lt;/p&gt;
&lt;p&gt;I started working on prototyping my own custom generator in GDScript. Initially, I created some simple Terrain via a noise map surrounded by a hexagonal wall representing the map edges. This was a good first setup in familiarizing myself with the voxel tooling.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Generation within a scoped area" src="/static/blog/2024-07-22/basic_generation_2.png" /&gt;&lt;/p&gt;
&lt;p&gt;Path generation ended up being a bit more complicated that I initially expected. For starters, I took two points and then just generated flat terrain between them. While this looked very unnatural, it boosted my confidence in my ability to work with the generation system as I wanted.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Path Generation" src="/static/blog/2024-07-22/path_generation.png" /&gt;&lt;/p&gt;
&lt;p&gt;At this point, I started working with multiple voxel types, which easily gave the world a lot more color, even with my programmer art. This will definitely be something I’ll be tweaking going forward, especially by implementing a system for having unique textures for each face.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Textured Generation" src="/static/blog/2024-07-22/textured_generation.png" /&gt;&lt;/p&gt;
&lt;p&gt;Next, I made the paths a little bit more natural by taking the noise value on the borders and smoothing to the center point. I initially didn’t get perfect results as seen below.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Silly Generation Bug" src="/static/blog/2024-07-22/silly_generation_bug.png" /&gt;&lt;/p&gt;
&lt;p&gt;But after making a few modifications, I was able to get something pretty nice, at least for terrain that isn’t too steep!&lt;/p&gt;
&lt;p&gt;&lt;iframe width="560" height="315" src="https://www.youtube.com/embed/MRlz2xZpgJ0" frameborder="0" allowfullscreen&gt;&lt;/iframe&gt;&lt;/p&gt;
&lt;p&gt;I was very happy with what I was able to achieve here, but the code was starting to get a bit bloated, which led to some performance issues. So with the concept proven, I decided to do some refactoring and getting the system running as a GDExtension.&lt;/p&gt;
&lt;h3 id="next-steps"&gt;Next Steps&lt;/h3&gt;
&lt;p&gt;Practically within the game, it very much resembles the early prototype but with much better performance. Without going into too many specifics, the refactor should allow for a more compartmentalized generation by going through difference layers of generation. The follow-up is to create those layers to start generating a good functional playground for more feature development.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Refactored Generation" src="/static/blog/2024-07-22/refactored_generation.png" /&gt;&lt;/p&gt;</content:encoded>
      <guid isPermaLink="false">/blog/2024-07-22_Generating_Voxel_Worlds</guid>
      <pubDate>Mon, 22 Jul 2024 00:00:00 +0053</pubDate>
    </item>
    <item>
      <title>Bluesky with RSS Readers</title>
      <link>https://layla.gg/blog/2025_03_04_bluesky_rss</link>
      <description>My journey of ingesting Bluesky feeds in to FreshRSS!</description>
      <content:encoded>&lt;p&gt;I've been looking at utilizing RSS feeds and RSS readers more to get a bit more control over my overall media diet. One of the platforms that I've found myself pulled towards regularly these days has been Bluesky, but instead of I wanted to aggregate it alongside much of the other content I read. Lucky for me, Bluesky supports RSS feeds for each person's profile. The only problem is there is not RSS feeds for the Bluesky feeds themselves.&lt;/p&gt;
&lt;p&gt;I found that I could create an OPML file that could be hosted on a web server to dynamically update a category with a list of feeds.&lt;/p&gt;
&lt;p&gt;Following that path, I went ahead and created a simple program in Golang that would utilize the Bluesky API. First it generates a session token from given login credentials then it simply gets a list of everyone the active user is following by using the &lt;code&gt;https://bsky.social/xrpc/app.bsky.graph.getFollows&lt;/code&gt; endpoint. Then it just adds their profile's RSS feed to an OPML file.&lt;/p&gt;
&lt;p&gt;The output of the program results in something looking like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-rss"&gt;&amp;lt;opml version=&amp;quot;2.0&amp;quot;&amp;gt;
    &amp;lt;head&amp;gt;
        &amp;lt;title&amp;gt;Bluesky Follows for @layla.gg&amp;lt;/title&amp;gt;
    &amp;lt;/head&amp;gt;
    &amp;lt;body&amp;gt;
        &amp;lt;outline text=&amp;quot;Ludum Dare&amp;quot; type=&amp;quot;rss&amp;quot; title=&amp;quot;ludumdare.com&amp;quot; xmlUrl=&amp;quot;https://bsky.app/profile/did:plc:bog7urgkzmru2akffvehygns/rss&amp;quot; htmlUrl=&amp;quot;https://bsky.app/profile/did:plc:bog7urgkzmru2akffvehygns&amp;quot;/&amp;gt;
        &amp;lt;outline text=&amp;quot;Brennan Lee Mulligan&amp;quot; type=&amp;quot;rss&amp;quot; title=&amp;quot;brennanleemulligan.bsky.social&amp;quot; xmlUrl=&amp;quot;https://bsky.app/profile/did:plc:4ca5dmcguk3q2fmbp5lcx65z/rss&amp;quot; htmlUrl=&amp;quot;https://bsky.app/profile/did:plc:4ca5dmcguk3q2fmbp5lcx65z&amp;quot;/&amp;gt;
    &amp;lt;/body&amp;gt;
&amp;lt;/opml&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can find the code and additional information here: &lt;a href="https://github.com/yeslayla/bluesky-opml"&gt;https://github.com/yeslayla/bluesky-opml&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;To keep the list current of feeds up-to-date, it is as simple as configuring a recurring tasks (like a cron) to execute a script to regenerate the OPML file on a regular interval.&lt;/p&gt;
&lt;p&gt;Plugging the result into FreshRSS resulted in this final product:&lt;/p&gt;
&lt;p&gt;&lt;img alt="Fresh RSS Screenshot" src="/static/blog/2025-03-04/freshrss_screenshot.png" /&gt;&lt;/p&gt;
&lt;h2 id="limitations"&gt;Limitations&lt;/h2&gt;
&lt;p&gt;Due to limitations of the RSS feed provided by Bluesky themselves, most posts don't contain any rich media elements. This means no images, videos, or clickable links. Perhaps this could be solved in the future with better support or perhaps a proxy service to format the content, but for now we just get the text.&lt;/p&gt;</content:encoded>
      <guid isPermaLink="false">/blog/2025_03_04_bluesky_rss</guid>
      <pubDate>Tue, 04 Mar 2025 00:00:00 +0053</pubDate>
    </item>
  </channel>
</rss>
