<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>Echoes of Ether</title>
        <link>https://paragraph.com/@echoesofether</link>
        <description>Echoes of Ether</description>
        <lastBuildDate>Sun, 07 Jun 2026 05:02:23 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <language>en</language>
        <image>
            <title>Echoes of Ether</title>
            <url>https://storage.googleapis.com/papyrus_images/7e9b70c67fd75cf113a4587a0310bce68b6fae5828e1d4b148704a8f212e60cc.jpg</url>
            <link>https://paragraph.com/@echoesofether</link>
        </image>
        <copyright>All rights reserved</copyright>
        <item>
            <title><![CDATA[Echoes of Ether - Devlog #1: The Mining Loop]]></title>
            <link>https://paragraph.com/@echoesofether/echoes-of-ether-devlog-1-the-mining-loop</link>
            <guid>33e3TAKPAxZM3k0x3m2t</guid>
            <pubDate>Thu, 04 Jun 2026 14:28:05 GMT</pubDate>
            <description><![CDATA[Echoes of the Ether - Devlog #1]]></description>
            <content:encoded><![CDATA[<p>The first thing I want to walk through is the mining loop, because right now it's the cleanest verified path I've got in the build. A player click that travels through five layers of code I wrote and comes back out as ore in the cargohold.</p><p>Here's a full cycle, idle to depletion:</p><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://youtu.be/iOcMrlmxFsw">https://youtu.be/iOcMrlmxFsw</a></p><h2 id="h-the-five-layers-i-keep-talking-about" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">The five layers I keep talking about</h2><p>When I say "the mining loop", I mean a chain that threads <code>spec → server → protocol → bridge → client</code>. I'm going to refer to these by name through the rest of the post, so quickly:</p><ul><li><p><strong>spec</strong>: my source of truth for cycle length, range, yield per tick, and what I count as a valid target.</p></li><li><p><strong>server</strong>: authoritative state. I keep the cycle, the asteroid's remaining ore, and the pilot's cargo here.</p></li><li><p><strong>protocol</strong>: the wire format the server and the client both compile against. Same crate, no drift.</p></li><li><p><strong>bridge</strong>: the boundary I use to translate server intent into client messages and back.</p></li><li><p><strong>client</strong>: input, rendering, and the small bits of optimistic state I use to make the game feel responsive.</p></li></ul><h2 id="h-the-path-ive-verified" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">The path I've verified</h2><p>What I can do right now, end to end:</p><ol><li><p>I target an asteroid and pick the mining laser from the hotbar.</p></li><li><p>The server validates: target exists, in mining-laser range (5km), laser fitted, capacitor has headroom.</p></li><li><p>The cycle starts. <code>S_MINING_CYCLE_PROGRESS</code> streams from server to client.</p></li><li><p>Each completed cycle adds ore to my hold server-side.</p></li><li><p>The 3D beam fires between my ship's hardpoint and the asteroid.</p></li><li><p>When the cycle ends, for any reason, <code>S_MINING_STOPPED</code> carries a typed reason byte back.</p></li><li><p>If the asteroid's ore hits zero, it despawns. The field replenishes per spec.</p></li></ol><h2 id="h-the-beam-is-doing-something-a-little-weird" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">The beam is doing something a little weird</h2><p>I want to be honest about how the beam actually works, because I'm not totally sure I love it.</p><p>When I click <code>mine</code>, the beam appears immediately. The client flips <code>is_mining = true</code> and starts rendering the beam <em>before</em> the server has confirmed anything. The server then sustains it via cycle-progress messages, and <code>S_MINING_STOPPED</code> is what kills it. If the server rejects my action, <code>mining_stopped</code> lands within a frame or two and clears the optimistic beam.</p><p>There's no beam opcode. I recompute the geometry (muzzle hardpoint to target position) client-side every render frame in <code>player_ship.gd:_update_mining_beam</code>. The server never sends beam coordinates.</p><p>So the precise framing for what I built: the beam is a pure client-side visual, shown optimistically on the player's own click, sustained by server cycle-progress, and extinguished by an authoritative stop reason. The server governs <em>mining state</em>. The client owns the line and redraws it itself.</p><p>I went back and forth on this. The argument for it: the player gets instant feedback on their own click, and a rejection lands fast enough that the desync window is invisible in practice. The argument against: a desync window exists, however small, and "invisible in practice" is the kind of phrase that ages badly under real network conditions. For now I'm leaving the optimistic version in because it feels right, but I want to revisit it once I get latency under real co-op load. idk if this is the shape it ends up in.</p><h2 id="h-typed-stop-reasons" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Typed stop reasons</h2><figure float="none" data-type="figure" class="img-center"><img src="https://storage.googleapis.com/papyrus_images/6b8e75f1814ba798c3c2561e2ba9e93f784b248defe30b9f839c384212ac1570.png" alt="Typed stop reasons" class="image-node embed"><figcaption htmlattributes="[object Object]" class="">Typed stop reasons</figcaption></figure><p>Every cycle I run ends for a specific reason, and I made that reason a typed enum on the wire instead of a string:</p><pre data-type="codeBlock" text="#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum MiningStopReason {
    Manual = 0,      // pilot manually stopped the cycle
    OutOfRange = 1,  // target drifted outside mining range
    Capacitor = 2,   // insufficient cap to keep cycling
    HoldFull = 3,    // ore hold reached capacity
    Destroyed = 4,   // asteroid depleted, no longer a valid target
    NoLaser = 5,     // no mining laser fitted (or it vanished)
}
"><code>#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub <span class="hljs-keyword">enum</span> <span class="hljs-title">MiningStopReason</span> {
    Manual <span class="hljs-operator">=</span> <span class="hljs-number">0</span>,      <span class="hljs-comment">// pilot manually stopped the cycle</span>
    OutOfRange <span class="hljs-operator">=</span> <span class="hljs-number">1</span>,  <span class="hljs-comment">// target drifted outside mining range</span>
    Capacitor <span class="hljs-operator">=</span> <span class="hljs-number">2</span>,   <span class="hljs-comment">// insufficient cap to keep cycling</span>
    HoldFull <span class="hljs-operator">=</span> <span class="hljs-number">3</span>,    <span class="hljs-comment">// ore hold reached capacity</span>
    Destroyed <span class="hljs-operator">=</span> <span class="hljs-number">4</span>,   <span class="hljs-comment">// asteroid depleted, no longer a valid target</span>
    NoLaser <span class="hljs-operator">=</span> <span class="hljs-number">5</span>,     <span class="hljs-comment">// no mining laser fitted (or it vanished)</span>
}
</code></pre><p>A generic "mining stopped" line in my logs is a bug magnet. I can't tell from it whether the player flew out of range, the asteroid emptied, the cap drained, or the cargohold filled up. Named variants make every cycle's tail inspectable. When I scale this loop to fleets and combat-site interrupts, I want to grep for <code>HoldFull</code> vs <code>OutOfRange</code> without parsing prose.</p><h2 id="h-how-well-tested-is-this-honestly" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">How well-tested is this, honestly</h2><p>Not very, lol. The mining loop is lightly tested compared to the rest of what I've built so far.</p><ul><li><p>3 tests I have directly on <code>systems/mining.rs</code>: <code>asteroid_depletes_after_its_stock_runs_out</code>, <code>no_laser_fitted_stops_with_no_laser_reason</code>, <code>non_ferrite_ore_flows_from_asteroid_to_event_and_hold</code>.</p></li><li><p>+1 data-validation guard (<code>runtime_warnings_flag_missing_mining_effect</code>) that fails my build if the mining effect data isn't wired.</p></li><li><p>+7 on the refining side that consume mined ore, ore-to-mineral conversion.</p></li></ul><p>So I've got about 3 tests directly on the mining loop, plus another 7 covering the refining path the ore feeds into. My combat code has 21 tests. Warp has 11. Mining isn't where I've put the test density, and I'd be flattering it if I said "the loop is covered". It works because the loop is short and the failure modes all live on typed <code>MiningStopReason</code> paths, but I want a lot more tests here before I trust it under real network strain.</p><h2 id="h-the-crack-i-havent-fixed-the-cargohold-gauge" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">The crack I haven't fixed: the cargohold gauge</h2><figure float="none" data-type="figure" class="img-center"><img src="https://storage.googleapis.com/papyrus_images/09fd70e15d453447f0779d15cf144ff16b12d774b6f8e104c4b3b177e201d930.png" alt="The cargohold gauge" class="image-node embed"><figcaption htmlattributes="[object Object]" class="">The cargohold gauge</figcaption></figure><p><code>S_CARGO_UPDATE</code> exists in my protocol. The client decodes it. The constant is defined. But the server never emits it, and the bridge explicitly drops it on the floor, so the hold count in the UI gets reconstructed one ore at a time from <code>MiningCycleCompleted.total_in_cargo</code> (<code>player_state.gd:310-313</code>).</p><p>What that means in practice:</p><ul><li><p>On the happy path, the gauge is right. The per-cycle total comes back authoritatively each tick.</p></li><li><p>Mid-cycle there's no whole-hold resync, so the "cargo full" toast I have in spec can't fire from a <code>CargoUpdate</code>. I have to infer it from <code>HoldFull</code>.</p></li><li><p>If anything mutates the hold <em>outside</em> a mining cycle and the client misses the event, my gauge drifts until the next authoritative snapshot.</p></li></ul><p>I don't think of this as its own bug. It's part of a cluster. I have about 14 server opcodes in the same shape: client-side decoder exists, no server-side emitter, the bridge silently swallows whatever the protocol said it could carry. The decoder for <code>S_CARGO_UPDATE</code> has been waiting for its emitter since day one.</p><p>So the credible fix isn't a one-line patch for cargo specifically. It's doing the whole decoder-without-emitter class at once, and I'd rather not promise that for Devlog 2. Bundling it into a single subsystem post would either rush the cleanup or pad the post. For now I'm naming the crack, bounding it, and leaving it on the list.</p><h2 id="h-next-week" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Next week</h2><p>I'm doing the combat cascade: shields, armor, hull, resists, module cycling, lock hysteresis, the death-to-respawn flow. Same teardown as this one, what I've verified and what's still cracked. Combat is where I've actually put the test density (21), so the honesty knob points the other way. The question stops being "did I test this enough" and starts being "why isn't the player <em>seeing</em> most of what my cascade does". That's the post.</p><p>Anyway, this is probably the most identifier-dense thing I'll write all month. The next ones get more pictures.</p>]]></content:encoded>
            <author>echoesofether@newsletter.paragraph.com (Ciefa)</author>
            <category>gamedev</category>
            <category>space</category>
            <category>indie</category>
            <enclosure url="https://storage.googleapis.com/papyrus_images/69df4eaee217ac1e357380da36035785e89a37a5d5a286739aab350414a2d8f4.jpg" length="0" type="image/jpg"/>
        </item>
    </channel>
</rss>