<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/"><channel><title>Isles of the Cloud Realm // Dev Journal</title><link>https://iotcr.com/dev-journal</link><description>Notes from building Isles of the Cloud Realm, an online RPG made with Go and Godot.</description><language>en-us</language><atom:link href="https://iotcr.com/dev-journal/feed.xml" rel="self" type="application/rss+xml" /><item><title>The Static DB</title><link>https://iotcr.com/dev-journal/the-static-db</link><guid isPermaLink="true">https://iotcr.com/dev-journal/the-static-db</guid><description>One source of truth for the server, the client, and the wiki</description><content:encoded><![CDATA[<p>Isles of the Cloud Realm is built as a Go server and a Godot client. The server is authoritative: it runs combat, validates actions, and persists state to a database. The client renders the world and handles player input.</p>
<p>Both need to agree on what exists in the game. Items, skills, gathering nodes, crafting recipes, etc. If I define an Iron Sword, the server needs its stats and equipment slot for combat math, while the client needs its name, icon, and stat bonuses for the inventory screen. If those definitions drift apart, the game breaks in annoying ways.</p>
<p>A third system reads the same definitions: the <a href="https://iotcr.com/wiki">public wiki</a> on this site, which has a page for every item, skill, node, and recipe.</p>
<p>I needed one source of truth. I call it the static-db.</p>
<figure><pre><code class="language-mermaid">flowchart LR
    YAML[("static-db&#x3C;br/>(YAML)")] --> Server["Go Server"]
    YAML --> Client["Godot Client"]
    YAML --> Wiki["Next.js Wiki"]
    Server --> DB[("PostgreSQL")]
    Server --> RAM["In-Memory Registries"]
    Client --> TRES[".tres Resources"]
</code></pre><figcaption>Mermaid diagram source. This renders as a diagram in the original post. <a href="https://iotcr.com/dev-journal/the-static-db">View it here.</a></figcaption></figure>
<h2>The Static DB</h2>
<p>The static-db is a directory of <a href="https://en.wikipedia.org/wiki/YAML">YAML</a> files. Every game entity gets a folder with an <code>entry.yml</code> and, optionally, an <code>icon.png</code>:</p>
<pre><code class="language-txt">static-db/database/
├── items/
│   ├── ingots/
│   │   ├── bronze/
│   │   │   ├── entry.yml
│   │   │   └── icon.png
│   │   └── iron/
│   │       ├── entry.yml
│   │       └── icon.png
│   └── equipment/
│       └── weapons/
│           └── swords/
│               └── iron/
│                   ├── entry.yml
│                   └── icon.png
├── skills/
│   └── smithing/
│       └── iron-platebody/
│           └── entry.yml
├── nodes/
│   └── trees/
│       └── oak/
│           ├── entry.yml
│           └── icon.png
└── stations/
    └── smithing/
        └── anvil/
            ├── entry.yml
            └── icon.png
</code></pre>
<p>This is the real entry for the <a href="https://iotcr.com/wiki/items/equipment/weapons/swords/iron">Iron Sword</a>:</p>
<pre><code class="language-yaml">id: 019bcddd-9931-7acd-92d1-3fbbccc161c8
name: Iron Sword
tier: 2
prerequisites:
  combat_level: 12
short_description: A humble sword, suited to any battle.
long_description: >
  Forged from @items/ingots/iron, this sword is a reliable
  weapon for any warrior. The blade is balanced and sharp,
  ready to strike true in combat.
item_slot: mainhand
common_stats:
  attack: 10
  agility: 4
  strength: 2
</code></pre>
<p>A few things are happening here.</p>
<ul>
<li>The <code>id</code> is a UUIDv7, generated once and used as the primary key wherever the item is persisted.</li>
<li>The path under <code>items/</code> (<code>equipment/weapons/swords/iron</code>) becomes its slug, used in database queries and client lookups. Wiki URLs and YAML <code>@</code> references keep the <code>items/</code> prefix.</li>
<li>The <a href="https://iotcr.com/wiki/items/ingots/iron">Iron Ingot</a> (<code>@items/ingots/iron</code>) in the description refers to another static-db entry.</li>
</ul>
<blockquote>
<p><strong>Note:</strong> This dev journal and wiki render <code>@</code> references as inline wiki links. The <a href="https://iotcr.com/wiki/items/ingots/iron">Iron Ingot</a> &#x26; <a href="https://iotcr.com/wiki/items/equipment/weapons/swords/iron">Iron Sword</a> above are live examples.</p>
</blockquote>
<p>Gathering nodes work the same way. Here's an <a href="https://iotcr.com/wiki/nodes/trees/oak">Oak Tree</a>:</p>
<pre><code class="language-yaml">id: 019f86ea-63aa-7ff9-8380-ad79a8fca5d7
name: Oak Tree
tier: 2
short_description: >
  A broad, sturdy hardwood with dense golden-brown timber.
long_description: >
  Oaks grow slow and strong in the richer soil of the larger
  isles, yielding @items/logs/oak. Their dense grain rewards
  a woodcutter with patience and a well-swung axe.
inspect_description: An Oak Tree with a broad, sturdy trunk.
prerequisites:
  woodcutting_level: 7
drop_table:
  - item: "@items/logs/oak"
    chance: 100
    quantity:
      min: 1
      max: 3
</code></pre>
<p>Crafting recipes connect those entries. This is the smithing recipe for <a href="https://iotcr.com/wiki/skills/smithing/iron-platebody">Forge Iron Platebody</a>:</p>
<pre><code class="language-yaml">id: 019bcde1-9b09-7294-bd68-21344182f886
name: Iron Platebody
tier: 2
level: 12
experience: 100
station: anvil
station_category: iron
rarity_eligibility: with_suffix
short_description: >
  Craft a heavy plate body armor from Iron ingots, providing
  excellent protection.
long_description: >
  A smithing technique that crafts @items/ingots/iron into a
  @items/equipment/chestpieces/plate-body/iron.
icon: "@items/equipment/chestpieces/plate-body/iron"
crafting:
  crafting_time: 5
  materials:
    - item: "@items/ingots/iron"
      count: 5
  produces:
    - item: "@items/equipment/chestpieces/plate-body/iron"
      count: 1
</code></pre>
<p>The <code>crafting</code> block says which materials are consumed, what gets produced, how long it takes, and which station it uses. The <code>@</code> references in <code>materials</code> and <code>produces</code> point to other entries. The whole graph is plain YAML.</p>
<p>Those references chain together into a graph, with recipes as the edges: each one consumes some entries and produces another. The platebody recipe above is a single branch off the iron ingot, which every iron recipe in the smithing tree draws from:</p>
<figure><pre><code class="language-mermaid">flowchart LR
    Deposit["Iron Deposit"] -->|"drops 1-3"| Ore["Iron Ore"]
    Ore -->|"2x"| Ingot["Iron Ingot"]
    Ingot -->|"1x"| Gloves["Iron Plate Gloves"]
    Ingot -->|"2x"| Helmet["Iron Plate Helmet"]
    Ingot -->|"4x"| Shield["Iron Plate Shield"]
    Ingot -->|"5x"| Plate["Iron Platebody"]
</code></pre><figcaption>Mermaid diagram source. This renders as a diagram in the original post. <a href="https://iotcr.com/dev-journal/the-static-db">View it here.</a></figcaption></figure>
<h2>Three Consumers</h2>
<p>Three systems read the same YAML and do different things with it.</p>
<p>The server reads the static-db in two ways. On startup, the sync code walks every <code>entry.yml</code> under <code>items/</code>, parses the YAML, and upserts each row into PostgreSQL by UUID. Gathering nodes, crafting recipes, and stations go into typed Go registries that stay in memory for the life of the server process. When a player chops an Oak Tree, the server looks up <code>oak</code> in the tree registry to get its drop table and skill requirements.</p>
<p>Bash build scripts compile the same YAML entries into Godot <code>.tres</code> resource files for the client. They walk the static-db, copy icons into the asset directory, and emit resources that Godot can load directly. If I change an item's name or stats, I run <code>make build</code> and the client picks it up.</p>
<p>The wiki is the simplest of the three. It reads the YAML directly at build time, with no intermediate compilation step. The Next.js site walks the directory tree, parses each file with <code>js-yaml</code>, and renders pages from the data.</p>
<p>When I add a new item, I write one <code>entry.yml</code> file and everything else picks it up.</p>
<h2>Schema and Validation</h2>
<p>YAML by itself is schemaless, and won't complain if you type <code>preqrequisites</code> instead of <code>prerequisites</code>. The rules for what each entry type requires are complicated enough that I can't keep them in my head. Items need an <code>item_slot</code>, equipment needs <code>prerequisites</code> on top of that, gathering nodes need a <code>drop_table</code>, crafting recipes need <code>materials</code> and <code>produces</code>. I made mistakes often enough to build a schema and linter.</p>
<p>Every <code>entry.yml</code> is validated against a layered <a href="https://json-schema.org/draft-07/json-schema-core.html">JSON Schema</a> hierarchy. The base schema requires an <code>id</code>, a <code>name</code>, and descriptions:</p>
<pre><code class="language-json">{
  "required": ["id", "name", "short_description", "long_description"],
  "properties": {
    "id": {
      "type": "string",
      "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-..."
    },
    "name": { "type": "string" },
    "short_description": { "type": "string" },
    "long_description": { "type": "string" }
  }
}
</code></pre>
<p>Each category adds its own requirements. Items need an <code>item_slot</code>, and gathering nodes need both a <code>drop_table</code> and <code>prerequisites</code>. Starting at each <code>entry.yml</code>, the validator walks up the directory tree, collects every <code>schema.json</code> it encounters, and merges them with <code>allOf</code>.</p>
<pre><code class="language-txt">static-db/database/
├── schema.json                  ← id, name, short/long_description
├── items/
│   ├── schema.json              ← item_slot, common_stats, max_stack
│   └── equipment/weapons/swords/iron/entry.yml
└── nodes/
    └── trees/
        ├── schema.json          ← inspect_description, drop_table
        └── oak/entry.yml
</code></pre>
<p>The tree schema extends the base with fields specific to trees:</p>
<pre><code class="language-json">{
  "allOf": [
    { "$ref": "../../schema.json" },
    {
      "required": ["inspect_description", "prerequisites", "drop_table"],
      "properties": {
        "prerequisites": {
          "required": ["woodcutting_level"],
          "properties": {
            "woodcutting_level": { "type": "integer", "minimum": 1 }
          },
          "additionalProperties": false
        },
        "drop_table": {
          "$ref": "../../_schemas/drop-table.schema.json"
        }
      }
    }
  ],
  "additionalProperties": false
}
</code></pre>
<p>The <code>additionalProperties: false</code> at the end catches typos. If I write <code>item_slo</code> instead of <code>item_slot</code>, the schema rejects it.</p>
<p><img src="https://iotcr.com/posts/item-slot-linter.png" alt="The linter catching a typo in item_slot."></p>
<blockquote>
<p>The linter catching a typo in item_slot.</p>
</blockquote>
<p>The linter verifies that every <code>@reference</code> points to a real <code>entry.yml</code>.</p>
<p><img src="https://iotcr.com/posts/reference-linter.png" alt="The linter catching a broken reference."></p>
<blockquote>
<p>The linter catching a broken reference.</p>
</blockquote>
<p>The linter also enforces formatting and line length. It runs on save in my editor and again in CI. I find mistakes there instead of waiting for something to break at runtime.</p>
<h2>The Wiki</h2>
<p>It feels a little strange to ship a <a href="https://iotcr.com/wiki">public wiki</a> before the game is available to anyone other than me.</p>
<p>The wiki reads the same YAML as the server and client, then renders a page for every item, skill, node, and station. Its purpose right now is selfish: it helps me keep things organized.</p>
<p>The game already has a few hundred entries, and that number keeps growing. When I'm working on combat and need to remember the stats on an <a href="https://iotcr.com/wiki/items/equipment/weapons/swords/iron">Iron Sword</a>, the level required to mine iron, or which recipes use <a href="https://iotcr.com/wiki/items/ingots/bronze">Bronze Ingot</a>, I don't want to dig through YAML files. I keep the wiki open while I work. It's faster than grep, and a page with icons, stat tables, and cross-references is much easier to scan than a wall of YAML.</p>
<p>It has also been useful when working with artists. Instead of passing around spreadsheets of item names and tiers, I give them access to the wiki. They can browse the icons and descriptions, and see how the items relate to each other.</p>
<h2>How Items Link Together</h2>
<p>The wiki turns <code>@</code> references into clickable links with icons and names. At build time it also scans the full database and computes reverse lookups for every entry:</p>
<ul>
<li>"Referenced By" lists entries that mention it in their description</li>
<li>"Material For" lists crafting recipes that use it as an input</li>
<li>"Item Sources" lists nodes that drop it and recipes that produce it</li>
</ul>
<p>None of this is stored. When I add a smithing recipe that consumes <a href="https://iotcr.com/wiki/items/ingots/iron">Iron Ingot</a>, the <a href="https://iotcr.com/wiki/items/ingots/iron">Iron Ingot</a> page picks it up under "Material For" without another edit. Recipe pages show the same graph from the other side: materials in, produces out, with chance and quantity ranges on node drops.</p>
<p>This is easier to show than to describe. Here is a <a href="https://iotcr.com/wiki/items/grain-sheaves/wheat">Wheat Sheaf</a>, which both has item sources and is a material for other recipes:</p>
<p><img src="https://iotcr.com/posts/wheat-sheaf-wiki-entry.png" alt="The Wheat Sheaf wiki entry, with Item Sources and Material For tables"></p>
<p>I don't have to keep the full crafting graph in my head. The wiki helps me traverse it.</p>
<h2>URLs as IDs</h2>
<p>The <a href="https://iotcr.com/wiki/items/equipment/weapons/swords/iron">Iron Sword</a> wiki URL is <code>/wiki/items/equipment/weapons/swords/iron</code>, which mirrors its static-db path: <code>items/equipment/weapons/swords/iron/entry.yml</code>. When the server syncs the item into PostgreSQL, it derives the slug from that same path: <code>equipment/weapons/swords/iron</code>.</p>
<p>It's the same path in four contexts:</p>
<table>
<thead>
<tr>
<th>Context</th>
<th>Path</th>
</tr>
</thead>
<tbody>
<tr>
<td>YAML reference</td>
<td><code>@items/equipment/weapons/swords/iron</code></td>
</tr>
<tr>
<td>Server item slug</td>
<td><code>equipment/weapons/swords/iron</code></td>
</tr>
<tr>
<td>Wiki URL</td>
<td><code>/wiki/items/equipment/weapons/swords/iron</code></td>
</tr>
<tr>
<td>Godot resource</td>
<td><code>resources/items/equipment/weapons/swords/iron.tres</code></td>
</tr>
</tbody>
</table>
<p>When the server needs to tell the client "you just received an <a href="https://iotcr.com/wiki/items/equipment/weapons/swords/iron">Iron Sword</a>," it sends the slug. The client uses it to look up the compiled resource for the icon and name. If I ever want to link to the wiki from the game, the URL is already there.</p>
<p>The directory path was a deliberate choice. One path that works as a database slug, a client resource lookup, a wiki URL, and a YAML reference, with no mapping table between them.</p>]]></content:encoded><pubDate>Sat, 29 Aug 2026 15:00:00 GMT</pubDate><dc:creator>Brandon Romano</dc:creator></item><item><title>Isles of the Cloud Realm</title><link>https://iotcr.com/dev-journal/isles-of-the-cloud-realm</link><guid isPermaLink="true">https://iotcr.com/dev-journal/isles-of-the-cloud-realm</guid><description>Building an Online RPG</description><content:encoded><![CDATA[<p>I'm building an online RPG. I'd never made a game before.</p>
<p>The indie game development community has told me this is inadvisable.</p>
<p><img src="https://iotcr.com/posts/login-screen.png" alt="The Isles of the Cloud Realm login screen"></p>
<h2>Late 2024</h2>
<p>In late 2024 I took a few-month sabbatical from work because I was burnt out. I spent the time on creative pursuits, to get away from thinking about computers, and it mostly worked; most of those months went to photography, writing, and sewing. My brain kept floating back anyway, to the same thought exercise: how would I go about designing a multiplayer game server?</p>
<p>I spent a few days building a small proof-of-concept. It came easily; this is what I've done professionally my whole career, and I understood what needed to get done and how to do it. It felt like a beautiful union between my adult domain (software engineering) and my childhood one (gaming, mostly online RPGs).</p>
<p>The client was new territory, and it's been a fun one. I'm trying to build the thinnest client I can, while the server stays in the driver's seat.</p>
<h2>Two Years In</h2>
<p>I'm coming up on two years of working on this consistently, and a world is starting to emerge from it.</p>
<p>I've recently hit the "this is starting to look like an actual game" phase, instead of it just feeling like a tech demo. It's a little odd to step back, look at it, and buy into the illusion.</p>
<p>It feels like the right moment to start talking about it.</p>
<p>The game side has come a long way too. Players sign in, make a character, and share a world that streams in around them. Many of the gathering and crafting skills are finished, and a real combat system is taking shape.</p>
<p><img src="https://iotcr.com/posts/harvesting-moongrain.png" alt="A player harvesting Moongrain"></p>
<p>There's still an extremely large amount of work remaining. The people who called this inadvisable weren't wrong about that part. Luckily, the process is where the joy is for me, and I'm not itchy to reach the end. The goal isn't to finish, it's to do the work. I expect that eventually adds up to a "finished" game.</p>
<h2>What the Game Will Feel Like</h2>
<p>I played too much <a href="https://en.wikipedia.org/wiki/RuneScape">Runescape</a> as a kid. What stayed with me was how deep the skilling and crafting went. Professions weren't just a side activity. You could pour hours into them without touching combat or questing, which is mostly how I played. I want that kind of depth.</p>
<p><a href="https://en.wikipedia.org/wiki/World_of_Warcraft">World of Warcraft</a> was the first game where the world itself felt like the main character instead of me. I think that matters in an online game. You should feel like you're part of something larger, not like everything revolves around you. Everyone else logged in is on a journey of their own, and the world can't be built so that each of you is its main character.</p>
<p>The combat system in <a href="https://en.wikipedia.org/wiki/Tibia_(video_game)">Tibia</a> was more creative than its constraints should have allowed: 2D, grid-locked, real-time, and still deep. You had to think about positioning, kiting, and using terrain to your advantage. Grid-locked movement doesn't have to mean simple gameplay.</p>
<p>I'm still pulling from new inspirations as I build, even ones I find long after I started. The gathering system in the <a href="https://en.wikipedia.org/wiki/Fantasy_Life">Fantasy Life</a> series feels super engaging; I only discovered that series once this project was already underway.</p>
<p>I'm not trying to clone any of them; what I want is the feeling they gave me. If I turn out to be twice as good as I think I am, I might manage half of it.</p>
<p>The game should be slow-paced and relaxing, not stressful or demanding. The kind of game you play while half-watching a show, or sink hours into when you're in the mood.</p>
<p>Progression still has to feel rewarding; there's a line between chill and boring that I'm trying to find. The focus will be PvE. Other players in the world should feel like they're on your team, not your competition. I might add PvP eventually, but that's not the initial focus.</p>
<h2>Go and Godot</h2>
<p>The server is Go, with no external libraries. The client is Godot, also with no external libraries.</p>
<p>The server is the source of truth. The client shows state in real-time with optimistic updates, and rolls back to server state when it needs to. It's a simple model, and it's worked well so far.</p>
<p>The world is 2D and grid-locked, like <a href="https://en.wikipedia.org/wiki/RuneScape">Runescape</a> and <a href="https://en.wikipedia.org/wiki/Tibia_(video_game)">Tibia</a>, but unlike a lot of grid-based games it isn't tick-based. It's real-time, and every entity has its own thread and can act independently. That decision has enabled some cool things I'll get into in future posts.</p>
<p>I skipped the libraries because I wanted to understand everything from the ground up. It's been a great way to learn, and it's enabled some important performance optimizations. From initial load testing, the server can support hundreds of thousands of simultaneous entities.</p>
<h2>What I'll Be Writing About</h2>
<p>A lot of it will be technical: architecture decisions, the problems I've run into, and what I've learned along the way. Plenty of it won't be. I'll write about game design too, how I'm thinking about the systems, what I want the game to feel like, and why I've made the calls I've made. I'm writing for other software engineers and game developers who are curious about how something like this gets built.</p>
<p>None of this is a game announcement. The game is far from ready for players, and I'm not willing to make any commitments about when, or if, it ever will be. This is a side project, and the fun is in the building. So there won't be release dates or feature roadmaps here, just the work as it happens.</p>
<h2>On Solo Dev</h2>
<p>I mostly work on this alone, but there's no such thing as truly solo development. I've commissioned artists for work I couldn't do myself, built parts of it alongside friends, and had enough in-person conversations about the game that there are pieces of it I couldn't trace back to any one person or conversation anymore.</p>
<p>I'm going to do my best to share it as I go.</p>]]></content:encoded><pubDate>Tue, 25 Aug 2026 15:00:00 GMT</pubDate><dc:creator>Brandon Romano</dc:creator></item></channel></rss>