<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="http://joekoop.com/blog/feed.xml" rel="self" type="application/atom+xml" /><link href="http://joekoop.com/blog/" rel="alternate" type="text/html" /><updated>2025-06-30T15:57:19+00:00</updated><id>http://joekoop.com/blog/feed.xml</id><title type="html">Joe’s Blog</title><subtitle>This is my blog. I don&apos;t know what I&apos;m doing.</subtitle><author><name>Joe Koop</name></author><entry><title type="html">Simple Remote Procedure Call</title><link href="http://joekoop.com/blog/2025/04/11/simple-remote-procedure-call.html" rel="alternate" type="text/html" title="Simple Remote Procedure Call" /><published>2025-04-11T00:00:00+00:00</published><updated>2025-04-11T00:00:00+00:00</updated><id>http://joekoop.com/blog/2025/04/11/simple-remote-procedure-call</id><content type="html" xml:base="http://joekoop.com/blog/2025/04/11/simple-remote-procedure-call.html"><![CDATA[<p><strong>Supersedes <a href="https://joekoop.com/blog/2023/10/18/spjc.html">SPJC</a></strong></p>

<p>SRPC is a JSON-based RPC protocol for clients, like webpages, to call a server via a standardized JSON interface. It requires a carrier protocol, like HTTP.</p>

<p>The keywords SHOULD, MAY, MUST, etc. are defined in <a href="https://www.rfc-editor.org/rfc/rfc2119">RFC 2119</a>.</p>

<h2 id="scope">Scope</h2>

<ul>
  <li>Successful response</li>
  <li>Successful response with warnings</li>
  <li>Error response</li>
  <li>Access control is not handled by SRPC</li>
  <li>Request deduplication is not handled by SRPC</li>
  <li>Rate limiting is not handled by SRPC</li>
</ul>

<h2 id="comparison-with-spjc">Comparison with SPJC</h2>

<p>SPJC is a similar protocol, but it encourages bad practices in the client code.</p>

<ul>
  <li>SPJC requires more complex code for both the server, and the client</li>
  <li>SPJC doesn’t require any kind of error handling in the client</li>
  <li>SRPC doesn’t describe any request aggregation or batching</li>
</ul>

<h2 id="schemas">Schemas</h2>

<h3 id="request">Request</h3>

<pre><code class="language-json">{
  "action": "string|undefined",
  "payload": "number|string|array|object|null"
  // all other properties are forbidden
}
</code></pre>

<h3 id="response">Response</h3>

<pre><code class="language-json">{
  "payload": "number|string|array|object|null|undefined",
  "error": "string|undefined",
  "warnings": "array&lt;string&gt;|undefined",
  "debug": "array&lt;string|object&gt;|undefined" // SHOULD not be used in a production environment

  // all other properties are forbidden
  // additionally, `payload`, and `error` are mutually exclusive, and at least one must be present
}
</code></pre>

<p>Clients MUST handle errors and warnings.</p>

<h2 id="efficient-payload-encoding">Efficient Payload Encoding</h2>

<p>This requires the carrier protocol to support Request and Response headers.</p>

<p>This spec does not describe a negotiation protocol; whether to use efficient payload encoding is an implementation detail.</p>

<p>When sending a large string, like a fragment of HTML, JSON-encoding it will cause it to expand in byte count by up to three times ($json \approx 3 \times plain$). To counter this, the sender MAY send the payload directly, without JSONizing it, if (and only if) there isn’t an error, and there aren’t any warnings or debug messages.</p>

<ol>
  <li>When sending only the payload, the sender MUST set a header to indicate that the payload is not JSONized: <code>X-SRPC-Raw-Payload: 1</code>.</li>
  <li>The request <code>action</code> is moved to the <code>X-SRPC-Action</code> header.</li>
</ol>

<h2 id="implementation">Implementation</h2>

<p>Below are some example implementations of the SRPC client and server.</p>

<h3 id="client">Client</h3>

<pre><code class="language-js">// this reference code assumes a few functions are set:
// display_error_to_user(string) - display an error to the user
// display_warning_to_user(string) - display a warning to the user

/**
 * @param {string} path
 * @param {string|null} action
 * @param {number|string|array|object|null} payload
 * @returns {Promise&lt;number|string|array|object|null|undefined&gt;} the response payload or undefined if there was an error
 */
async function srpc(path, action, payload) {
  try {
    if (
      typeof payload == "string" &amp;&amp;
      payload.length &gt; 1024 &amp;&amp;
      action?.length &lt; 200
    ) {
      // efficient payload encoding

      let response = await fetch(path, {
        method: "POST",
        headers: {
          Accept: "application/json, application/octet-stream",
          "Content-Type": "application/octet-stream",
          "X-SRPC-Action": action ?? "", // maybe just don't set this header if action is null
          "X-SRPC-Raw-Payload": "1",
        },
        body: payload,
        redirect: "error",
      });
    } else {
      // JSON-encoding

      let response = await fetch(path, {
        method: "POST",
        headers: {
          Accept: "application/json, application/octet-stream",
          "Content-Type": "application/json",
        },
        body: JSON.stringify({ action, payload }),
        redirect: "error",
      });
    }

    if (response.headers.get("X-SRPC-Raw-Payload") == "1") {
      return await response.text();
    }

    response = await response.json();

    if (response.error) {
      console.error(response.error);
      display_error_to_user(response.error);
      return undefined;
    }

    if (response.warnings) {
      for (let warning of response.warnings) {
        console.warn(warning);
        display_warning_to_user(warning);
      }
    }

    if (response.debug) {
      for (let debug of response.debug) {
        console.debug(debug);
      }
    }

    return response.payload;
  } catch (err) {
    display_error_to_user("An error occurred: " + err.message);
  }
}
</code></pre>

<h3 id="server">Server</h3>

<pre><code class="language-php">// written for Laravel, but should be portable to other frameworks

use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Symfony\Component\HttpFoundation\Request;

// this class handles receiving requests from a client, not sending a request to another server
final class SrpcRequest extends Request {
  public readonly string|null $action;
  public readonly int|float|array|object|string|null $payload;

  public function __construct(...$params) {
    if (count($params) &lt; 1) {
      parent::__construct(
        query: request()-&gt;query-&gt;all(),
        request: request()-&gt;request-&gt;all(),
        attributes: request()-&gt;attributes-&gt;all(),
        cookies: request()-&gt;cookies-&gt;all(),
        files: request()-&gt;files-&gt;all(),
        server: request()-&gt;server-&gt;all(),
        content: request()-&gt;getContent(),
      );
    } else {
      parent::__construct(...$params);
    }

    $this-&gt;action = parent::get("action", null);
    $this-&gt;payload = parent::get("payload", null);
  }

  /**
   * Validate the contents of the payload. Assumes the payload is an object.
   */
  public function validate($rules, ...$params): array {
    $newRules = [];
    foreach ($rules as $key =&gt; $rule) {
      $newRules["payload." . $key] = $rule;
    }
    return parent::validate($newRules, ...$params);
  }

  /**
   * Call Laravel's validator on the payload.
   */
  public function validatePayload(string $rules): void {
    parent::validate(["payload" =&gt; $rules]);
  }

  /**
   * Fallback: Get a value from the payload. Not recommended; use -&gt;payload-&gt;key instead.
   */
  public function __get($key): int|float|null|array|object|string {
    return $this-&gt;get("payload." . $key, null);
  }
}

final class SrpcResponse extends Response {
  private bool $payloadOrErrorIsSet = false;
  private mixed $payload = null;
  private string|null $error = null;
  private array $warnings = [];
  private array $debug = [];

  // Recommended use in your controller:
  // return $response-&gt;withPayload("Saved successfully");
  public function withPayload(mixed $payload): self {
    if ($this-&gt;payloadOrErrorIsSet) {
      throw new \Exception("Payload or error already set");
    }

    $this-&gt;payload = $payload;
    $this-&gt;payloadOrErrorIsSet = true;
    return $this;
  }

  // Recommended use in your controller:
  // return $response-&gt;withError("Couldn't save: collision");
  public function withError(string $error): self {
    if ($this-&gt;payloadOrErrorIsSet) {
      throw new \Exception("Payload or error already set");
    }

    $this-&gt;error = $error;
    $this-&gt;payloadOrErrorIsSet = true;
    return $this;
  }

  // Recommended use in your controller:
  // $response-&gt;addWarning('Format of postal code was corrected to "A1A 1A1"');
  public function addWarning(string $warning): void {
    $this-&gt;warnings[] = $warning;
  }

  // Recommended use in your controller:
  // $response-&gt;addDebug("Your user ID is " . Auth::id());
  public function addDebug(string|object $debug): void {
    $this-&gt;debug[] = $debug;
  }

  // called by Laravel automatically
  public function prepare(Request $request): static {
    if ($this-&gt;payloadOrErrorIsSet == false) {
      throw new \Exception("Payload or error not set");
    }

    if (
      gettype($this-&gt;payload) == "string" &amp;&amp;
      strlen($this-&gt;payload) &gt; 1024 &amp;&amp;
      empty($this-&gt;warnings) &amp;&amp;
      empty($this-&gt;debug)
    ) {
      $this-&gt;headers-&gt;set("X-SRPC-Raw-Payload", "1");
      $this-&gt;headers-&gt;set("Content-Type", "application/octet-stream");
      $this-&gt;setContent($this-&gt;payload);
    } else {
      $this-&gt;headers-&gt;set("Content-Type", "application/json");

      $theResponse = [];
      if ($this-&gt;error != null) {
        $theResponse["error"] = $this-&gt;error;
      } else {
        $theResponse["payload"] = $this-&gt;payload;
      }
      if (!empty($this-&gt;warnings)) {
        $theResponse["warnings"] = $this-&gt;warnings;
      }
      if (!empty($this-&gt;debug)) {
        $theResponse["debug"] = $this-&gt;debug;
      }

      $this-&gt;setContent(json_encode($theResponse));
    }

    return parent::prepare($request);
  }
}
</code></pre>]]></content><author><name>Joe Koop</name></author><category term="api-schema" /><summary type="html"><![CDATA[A simple application-layer message-passing protocol]]></summary></entry><entry><title type="html">Parsing JSON in Zig isn’t Hard</title><link href="http://joekoop.com/blog/2024/07/23/parsing-json-in-zig-isnt-hard.html" rel="alternate" type="text/html" title="Parsing JSON in Zig isn’t Hard" /><published>2024-07-23T00:00:00+00:00</published><updated>2024-07-23T00:00:00+00:00</updated><id>http://joekoop.com/blog/2024/07/23/parsing-json-in-zig-isnt-hard</id><content type="html" xml:base="http://joekoop.com/blog/2024/07/23/parsing-json-in-zig-isnt-hard.html"><![CDATA[<pre><code class="language-plain">&gt; zig version
0.13.0
</code></pre>

<p>While working on <!-- the foundations of my web analytics server, [OK Analytics][ok-analytics] --> a project of mine, I needed to add functionality to parse JSON. As it turns out, it’s not very hard once you figure out how <code>std.json</code> wants to do it.</p>

<p><strong>Q:</strong> What do you mean, “it’s not very hard”? I know <a href="https://ziglang.org/documentation/0.13.0/std/#std.json"><code>std.json</code></a> exists, but it’s very tedious, isn’t it? You get an iterator of “tokens” that you have to match to your preferred type, yourself, right?</p>

<p><strong>A:</strong> Yes, but actually no.</p>

<p>You could handle the token stream yourself if you really wanted to, but if your JSON document is relatively small, you could pass it as a <code>[]u8</code> to <code>std.json.parseFromSlice</code>, or <code>...parseFromSliceLeaky</code>, depending or your memory situation, like this:</p>

<pre><code class="language-zig">const std = @import("std");

test "parseFromSliceLeaky u16" {
    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
    defer arena.deinit();
    const allocator = arena.allocator();
    const result = try std.json.parseFromSliceLeaky(u16, allocator, "1234", .{});
    try std.testing.expect(result == 1234);
}
</code></pre>

<p>In fact, it even handles structs:</p>

<pre><code class="language-zig">const std = @import("std");

const Contact = struct {
    id: u64,
    first_name: []u8,
    last_name: ?[]u8 = null,
    phone_numbers: []struct {
        type: enum { home, mobile, work },
        number: []u8,
    },
    custom_fields: std.json.ArrayHashMap([]u8),
};

test "parseFromSliceLeaky Contact" {
    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
    defer arena.deinit();
    const allocator = arena.allocator();
    const result = try std.json.parseFromSliceLeaky(Contact, allocator,
        \\{
        \\  "id": 1234,
        \\  "first_name": "John",
        \\  "phone_numbers": [
        \\      { "type": "home", "number": "+18885550123" },
        \\      { "type": "mobile", "number": "+18885550189" }
        \\  ],
        \\  "custom_fields": {
        \\      "Hat size": "7\u00bc"
        \\  }
        \\}
    , .{});
    try std.testing.expect(result.id == 1234);
    try std.testing.expect(std.mem.eql(u8, result.first_name, "John"));
    try std.testing.expect(result.last_name == null);
    try std.testing.expect(result.phone_numbers[0].type == .home);
    try std.testing.expect(std.mem.eql(u8, result.phone_numbers[0].number, "+18885550123"));
    try std.testing.expect(result.phone_numbers[1].type == .mobile);
    try std.testing.expect(std.mem.eql(u8, result.phone_numbers[1].number, "+18885550189"));
    try std.testing.expect(std.mem.eql(u8, result.custom_fields.map.get("Hat size") orelse "", "7¼"));
}
</code></pre>

<p>I tried to include every type of data that you’d want to parse from a JSON document.</p>

<p><strong>TIP</strong><br />
There are some gotchas here: Nothing has a default value out-of-the-box. Even if you define something as being optional, if the JSON document doesn’t specify a value for it, it won’t default to <code>null</code>; it’ll cause <code>std.json.&lt;whatever&gt;</code> to return a <code>MissingField</code> error.</p>]]></content><author><name>Joe Koop</name></author><category term="json" /><category term="webdev" /><category term="zig" /><summary type="html"><![CDATA[Seriously, it's super easy. Just do it like this]]></summary></entry><entry><title type="html">Single endPoint JSON Communication</title><link href="http://joekoop.com/blog/2023/10/18/spjc.html" rel="alternate" type="text/html" title="Single endPoint JSON Communication" /><published>2023-10-18T00:00:00+00:00</published><updated>2023-10-18T00:00:00+00:00</updated><id>http://joekoop.com/blog/2023/10/18/spjc</id><content type="html" xml:base="http://joekoop.com/blog/2023/10/18/spjc.html"><![CDATA[<p><strong>Superseded by <a href="https://joekoop.com/blog/2025/04/11/simple-remote-procedure-call.html">SRPC</a></strong></p>

<p>Single endPoint JSON Communication (SPJC) is a simple application-layer message-passing protocol that uses HTTP (<a href="https://datatracker.ietf.org/doc/html/rfc9110">RFC 9110</a>) and is intended for web2 applications. The passing of a message always begins with the client. The server is unable to send messages without being called (the client will have to poll the server).</p>

<p>The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in <a href="https://datatracker.ietf.org/doc/html/rfc2119">RFC 2119</a>.</p>

<h2 id="1-message-from-a-client">1. Message from a client</h2>

<p>A message from a client is an HTTP <code>SPJC</code> request, with a <a href="https://www.json.org/json-en.html">JSON</a> body.</p>

<ol>
  <li>The request method SHOULD be <code>SPJC</code> (see §3 below), but MAY be <code>POST</code>
    <ol>
      <li>The request method MUST NOT be anything other than <code>SPJC</code> or <code>POST</code></li>
    </ol>
  </li>
  <li>The body MUST be JSON format</li>
  <li>The request path SHOULD NOT contain any IDs</li>
  <li>This request header SHOULD be sent: <code>Content-Type: application/json</code></li>
  <li>This request header SHOULD be sent: <code>Cache-Control: no-cache</code></li>
  <li>The server MUST ignore all request headers, except where impractical*, except
    <ol>
      <li>The server MAY consider the value of the Authorization header</li>
    </ol>
  </li>
</ol>

<p>This document does not impose any limits to the length/size of the request, although keep in mind that servers MAY impose their own limits.</p>

<p>*where impractical: for example, infrastructure headers, like Host</p>

<h2 id="2-message-from-a-server">2. Message from a server</h2>

<p>A message from a server is an HTTP response, with a JSON body.</p>

<ol>
  <li>The HTTP status code SHOULD be 200</li>
  <li>The HTTP status code MUST be ignored by the client, except
    <ol>
      <li>Iff the message from the client was sent via an <code>SPJC</code> request, the status code 501 MAY be considered for deciding to repeat the message via a <code>POST</code> request</li>
    </ol>
  </li>
  <li>The body MUST be JSON format</li>
  <li>This response header SHOULD be sent: <code>Content-Type: application/json</code></li>
  <li>This response header SHOULD be sent: <code>Cache-Control: max-age=0, no-cache, must-revalidate, proxy-revalidate</code> or equivalent-in-spirit</li>
  <li>The body (after parsing), SHOULD be an object
    <ol>
      <li>The object SHOULD have a <code>success</code> key/value
        <ol>
          <li>The value of <code>success</code> SHOULD be a boolean indicating whether or not something went wrong when generating the response</li>
        </ol>
      </li>
      <li>When <code>success</code> is false, the object SHOULD have a <code>message</code> key/value
        <ol>
          <li>The value of <code>message</code> SHOULD be a string.</li>
        </ol>
      </li>
    </ol>
  </li>
</ol>

<h2 id="3-spjc-method">3. SPJC Method</h2>

<p>SPJC is a non-standard HTTP method. See also <a href="https://datatracker.ietf.org/doc/html/rfc9110#name-method-extensibility">RFC 9110 § 16.1. Method Extensibility</a></p>

<ul>
  <li>SPJC method is not “safe”</li>
  <li>SPJC method is not “idempotent”</li>
  <li>Responses to SPJC method are not cachable</li>
</ul>

<h2 id="example-code">Example Code</h2>

<h3 id="client-helper-function">Client helper function</h3>

<pre><code class="language-js">/**
 * Send data to the server, and get its response
 *
 * @param path string
 * @param data any
 * @return object|false
 */
async function spjc(path, data) {
    try {
        let response = await fetch(path, {
            method: "SPJC",
            headers: {
                "Content-Type": "application/json",
                "Cache-Control": "no-cache",
            },
            body: JSON.stringify(data),
            redirect: "manual", // ignore redirections
        });
        let body = await response.json();
        if (typeof body != "object") return false;
        if (typeof body.success != "boolean") return false;
        if (body.success != true) return false;
        return body;
    } catch (e) {
        console.error(e);
        return false;
    }
}
</code></pre>]]></content><author><name>Joe Koop</name></author><category term="api-schema" /><summary type="html"><![CDATA[A simple application-layer message-passing protocol]]></summary></entry><entry><title type="html">Simple ERD</title><link href="http://joekoop.com/blog/2023/02/03/simple-erd.html" rel="alternate" type="text/html" title="Simple ERD" /><published>2023-02-03T00:00:00+00:00</published><updated>2023-02-03T00:00:00+00:00</updated><id>http://joekoop.com/blog/2023/02/03/simple-erd</id><content type="html" xml:base="http://joekoop.com/blog/2023/02/03/simple-erd.html"><![CDATA[<p>Simplified ERD is designed for fast writing (drawing?) of ERD ideas and is based on <a href="https://en.wikipedia.org/wiki/Entity%E2%80%93relationship_model#Crow's_foot_notation">Crow’s Foot ERD</a>, but with these key differences:</p>

<ul>
  <li>The arrow heads are easier to remember</li>
  <li>There aren’t redundant / badly described arrow heads</li>
  <li>A full entity definition isn’t required</li>
</ul>

<h2 id="arrow-heads">Arrow Heads</h2>

<table>
  <thead>
    <tr>
      <th>Symbol</th>
      <th>Description / Designation</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code>---|O</code></td>
      <td>zero or one</td>
    </tr>
    <tr>
      <td><code>--||O</code></td>
      <td>zero or more</td>
    </tr>
    <tr>
      <td><code>----|</code></td>
      <td>exactly one</td>
    </tr>
    <tr>
      <td><code>---||</code></td>
      <td>one or more</td>
    </tr>
  </tbody>
</table>

<h2 id="examples">Examples</h2>

<h3 id="example-1">Example 1</h3>

<ul>
  <li>Users each have exactly one language</li>
  <li>Languages can each have many users</li>
</ul>

<pre><code class="language-plain">user O||----| language
</code></pre>

<h3 id="example-2">Example 2</h3>

<ul>
  <li>Users each have at least one email address</li>
  <li>Email addresses each have at most one user</li>
</ul>

<pre><code class="language-plain">user O|----|| email_address
</code></pre>]]></content><author><name>Joe Koop</name></author><category term="notation" /><summary type="html"><![CDATA[Easier to remember and faster to write than crow's foot]]></summary></entry><entry><title type="html">Nodecore Recipes</title><link href="http://joekoop.com/blog/2021/10/18/nodecore-recipes.html" rel="alternate" type="text/html" title="Nodecore Recipes" /><published>2021-10-18T00:00:00+00:00</published><updated>2021-10-18T00:00:00+00:00</updated><id>http://joekoop.com/blog/2021/10/18/nodecore-recipes</id><content type="html" xml:base="http://joekoop.com/blog/2021/10/18/nodecore-recipes.html"><![CDATA[<style>
    article tr.border-on-top {
        border-top: 1px solid grey;
    }

    article td {
        padding: 0.5em 0;
        vertical-align: top;
    }

    article table ul {
        margin-block: 0;
    }

    @media only screen and (min-width: 800px) {
        article ul {
            width: max-content;
        }
    }
</style>

<p>The last time I checked, <a href="https://nodecore.mine.nu/wiki/index.php/Main_Page">the official wiki</a> was very broken, so my brother and I brute-forced our way through the game, and wrote this recipe book while we were at it. This page is incomplete.</p>

<p>Recipes tested on <a href="https://content.minetest.net/packages/warr1024/nodecore">NodeCore</a> release 5310 (game.conf) with a few mods:</p>

<ul>
  <li><a href="https://content.minetest.net/packages/Winter94/nc_light">nc_light</a></li>
  <li><a href="https://content.minetest.net/packages/Avicennia_g/nc_stucco">nc_stucco</a></li>
  <li><a href="https://content.minetest.net/packages/Warr1024/nc_ziprunes">nc_ziprunes</a></li>
</ul>

<table style="border-collapse:collapse"><tbody><tr><th>Required by</th><th>Name</th><th>Requires</th><th>Instructions</th></tr><tr id="adobe" class="border-on-top"><td rowspan="1"><ul></ul></td><td rowspan="1">Adobe</td><td><ul><li>1
<span style="color:red">Ash</span></li><li>1
<a href="#loosedirt">Loose Dirt</a></li><li>1
<a href="#woodenmallet">Wooden Mallet</a></li></ul></td><td>Place a loose dirt on an ash. Pummel with a mallet</td></tr><tr id="aggregate" class="border-on-top"><td rowspan="1"><ul></ul></td><td rowspan="1">Aggregate</td><td><ul><li>1
<span style="color:red">Ash</span></li><li>1
<span style="color:red">Loose Gravel</span></li><li>1
<a href="#woodenmallet">Wooden Mallet</a></li></ul></td><td>Place a loose gravel on an ash. Pummel with a mallet</td></tr><tr id="charcoal" class="border-on-top"><td rowspan="1"><ul><li><a href="#charcoallump">Charcoal Lump</a></li></ul></td><td rowspan="1">Charcoal</td><td><ul><li>1
<a href="#fire">Fire</a></li></ul></td><td>Create a fire. Put out the fire by surrounding it with non-combustible material</td></tr><tr id="charcoalblock" class="border-on-top"><td rowspan="1"><ul><li><a href="#charcoallump">Charcoal Lump</a></li></ul></td><td rowspan="1">Charcoal Block</td><td><ul><li>8
<a href="#charcoallump">Charcoal Lump</a></li><li>1
<a href="#woodenmallet">Wooden Mallet</a></li></ul></td><td>Drop 8 charcoal lump. Pummel them with a mallet</td></tr><tr id="charcoallump" class="border-on-top"><td rowspan="2"><ul><li><a href="#charcoalblock">Charcoal Block</a></li><li><a href="#torch">Torch</a></li></ul></td><td rowspan="2">Charcoal Lump</td><td><ul><li>1
<a href="#charcoalblock">Charcoal Block</a></li><li>1
<a href="#woodenhatchet">Wooden Hatchet</a></li></ul></td><td>Pummel a charcoal block with a hatchet [makes 8]</td></tr><tr><td><ul><li>1
<a href="#charcoal">Charcoal</a></li><li>1
<a href="#woodenhatchet">Wooden Hatchet</a></li></ul></td><td>Pummel a charcoal with a hatchet</td></tr><tr id="chromaticglass" class="border-on-top"><td rowspan="1"><ul></ul></td><td rowspan="1">Chromatic Glass</td><td><ul><li>1
<a href="#moltenglass">Molten Glass</a></li><li>1
<a href="#water">Water</a></li></ul></td><td>Cool a still (not flowing) molten glass with water</td></tr><tr id="cleanglass" class="border-on-top"><td rowspan="1"><ul></ul></td><td rowspan="1">Clean Glass</td><td><ul><li>1
<a href="#moltenglass">Molten Glass</a></li></ul></td><td>Let a molten glass cool while still (not flowing)</td></tr><tr id="crudeglass" class="border-on-top"><td rowspan="1"><ul></ul></td><td rowspan="1">Crude Glass</td><td><ul><li>1
<a href="#moltenglass">Molten Glass</a></li></ul></td><td>Let a molten glass cool while flowing</td></tr><tr id="dirt" class="border-on-top"><td rowspan="2"><ul><li><a href="#loosedirt">Loose Dirt</a></li></ul></td><td rowspan="2">Dirt</td><td><ul></ul></td><td>A naturally occurring resource. Discover as part of the grassy ground</td></tr><tr><td><ul><li>1
<a href="#loosedirt">Loose Dirt</a></li><li>1
<a href="#woodenmallet">Wooden Mallet</a></li></ul></td><td>Pummel a loose dirt with a mallet</td></tr><tr id="eggcorn" class="border-on-top"><td rowspan="1"><ul><li><a href="#plantedeggcorn">Planted Eggcorn</a></li></ul></td><td rowspan="1">Eggcorn</td><td><ul><li>1
<a href="#leaves">Leaves</a></li></ul></td><td>Mine a leaves with your hand. There is a chance of it turning into an eggcorn. The closer to the tree trunk the higher the chance</td></tr><tr id="fire" class="border-on-top"><td rowspan="1"><ul><li><a href="#charcoal">Charcoal</a></li><li><a href="#moltenglass">Molten Glass</a></li></ul></td><td rowspan="1">Fire</td><td><ul><li>1
<a href="#littorch">Lit Torch</a></li><li>1
<span style="color:red">group:Burnable</span></li></ul></td><td>Place a lit torch beside a group:burnable</td></tr><tr id="leaves" class="border-on-top"><td rowspan="1"><ul><li><a href="#eggcorn">Eggcorn</a></li><li><a href="#looseleaves">Loose Leaves</a></li><li><a href="#stick">Stick</a></li></ul></td><td rowspan="1">Leaves</td><td><ul><li>1
<a href="#tree">Tree</a></li></ul></td><td>A natural resource. Discover as part of a tree</td></tr><tr id="littorch" class="border-on-top"><td rowspan="1"><ul><li><a href="#fire">Fire</a></li></ul></td><td rowspan="1">Lit Torch</td><td><ul><li>1
<a href="#staff">Staff</a></li><li>1
<a href="#torch">Torch</a></li></ul></td><td>Pummel a torch with a staff</td></tr><tr id="log" class="border-on-top"><td rowspan="2"><ul><li><a href="#woodenplank">Wooden Plank</a></li></ul></td><td rowspan="2">Log</td><td><ul><li>1
<a href="#treetrunk">Tree Trunk</a></li><li>1
<a href="#woodenhatchet">Wooden Hatchet</a></li></ul></td><td>Mine a tree trunk with a hatchet</td></tr><tr><td><ul><li>1
<a href="#treetrunk">Tree Trunk</a></li></ul></td><td>Mine a tree trunk with your hand. This takes a very long time</td></tr><tr id="loosedirt" class="border-on-top"><td rowspan="2"><ul><li><a href="#adobe">Adobe</a></li><li><a href="#dirt">Dirt</a></li><li><a href="#plantedeggcorn">Planted Eggcorn</a></li></ul></td><td rowspan="2">Loose Dirt</td><td><ul><li>1
<a href="#dirt">Dirt</a></li><li>1
<a href="#woodenspade">Wooden Spade</a></li></ul></td><td>Mine a dirt with a spade</td></tr><tr><td><ul><li>1
<a href="#dirt">Dirt</a></li></ul></td><td>Mine a dirt with your hand</td></tr><tr id="looseleaves" class="border-on-top"><td rowspan="1"><ul><li><a href="#peat">Peat</a></li></ul></td><td rowspan="1">Loose Leaves</td><td><ul><li>1
<a href="#leaves">Leaves</a></li></ul></td><td>Mine a leaves with your hand. Leaves may turn into a stick or an eggcorn when mined</td></tr><tr id="loosesand" class="border-on-top"><td rowspan="2"><ul><li><a href="#moltenglass">Molten Glass</a></li><li><a href="#render">Render</a></li></ul></td><td rowspan="2">Loose Sand</td><td><ul><li>1
<a href="#sand">Sand</a></li><li>1
<a href="#woodenspade">Wooden Spade</a></li></ul></td><td>Mine a sand with a spade</td></tr><tr><td><ul><li>1
<a href="#sand">Sand</a></li></ul></td><td>Mine a sand with your hand</td></tr><tr id="moltenglass" class="border-on-top"><td rowspan="1"><ul><li><a href="#chromaticglass">Chromatic Glass</a></li><li><a href="#cleanglass">Clean Glass</a></li><li><a href="#crudeglass">Crude Glass</a></li></ul></td><td rowspan="1">Molten Glass</td><td><ul><li>2
<a href="#fire">Fire</a></li><li>1
<a href="#loosesand">Loose Sand</a></li></ul></td><td>Place a loose sand and 2 fire at 2 corners of the sand. Wait</td></tr><tr id="peat" class="border-on-top"><td rowspan="1"><ul></ul></td><td rowspan="1">Peat</td><td><ul><li>8
<a href="#looseleaves">Loose Leaves</a></li><li>1
<a href="#woodenadze">Wooden Adze</a></li></ul></td><td>Drop 8 loose leaves. Pummel them with an adze</td></tr><tr id="plantedeggcorn" class="border-on-top"><td rowspan="1"><ul><li><a href="#tree">Tree</a></li></ul></td><td rowspan="1">Planted Eggcorn</td><td><ul><li>1
<a href="#eggcorn">Eggcorn</a></li><li>1
<a href="#loosedirt">Loose Dirt</a></li></ul></td><td>Place an eggcorn 1 block down in the ground. Place a loose dirt on the eggcorn</td></tr><tr id="render" class="border-on-top"><td rowspan="1"><ul></ul></td><td rowspan="1">Render</td><td><ul><li>1
<span style="color:red">Ash</span></li><li>1
<a href="#loosesand">Loose Sand</a></li><li>1
<a href="#woodenmallet">Wooden Mallet</a></li></ul></td><td>Place a loose sand on an ash. Pummel with a mallet</td></tr><tr id="sand" class="border-on-top"><td rowspan="1"><ul><li><a href="#loosesand">Loose Sand</a></li></ul></td><td rowspan="1">Sand</td><td><ul></ul></td><td>A naturally occurring resource. Discover as part of the ground near a river or sea</td></tr><tr id="staff" class="border-on-top"><td rowspan="1"><ul><li><a href="#littorch">Lit Torch</a></li><li><a href="#torch">Torch</a></li><li><a href="#woodenadze">Wooden Adze</a></li><li><a href="#woodenframe">Wooden Frame</a></li><li><a href="#woodenhatchet">Wooden Hatchet</a></li><li><a href="#woodenmallet">Wooden Mallet</a></li><li><a href="#woodenspade">Wooden Spade</a></li></ul></td><td rowspan="1">Staff</td><td><ul><li>2
<a href="#stick">Stick</a></li></ul></td><td>Place a stick on the top of a stick</td></tr><tr id="stick" class="border-on-top"><td rowspan="2"><ul><li><a href="#staff">Staff</a></li><li><a href="#woodenadze">Wooden Adze</a></li></ul></td><td rowspan="2">Stick</td><td><ul><li>1
<a href="#leaves">Leaves</a></li></ul></td><td>Mine a leaves with your hand. There is a chance of it turning into a stick. The closer to the tree trunk the higher the chance</td></tr><tr><td><ul><li>1
<span style="color:red">Stone-Tipped Mallet</span></li><li>1
<a href="#woodenplank">Wooden Plank</a></li></ul></td><td>Pummel the top of a wooden plank with a stone-tipped mallet or better [makes 8]</td></tr><tr id="torch" class="border-on-top"><td rowspan="1"><ul><li><a href="#littorch">Lit Torch</a></li></ul></td><td rowspan="1">Torch</td><td><ul><li>1
<a href="#charcoallump">Charcoal Lump</a></li><li>1
<a href="#staff">Staff</a></li></ul></td><td>Place a charcoal lump on the top of a staff</td></tr><tr id="tree" class="border-on-top"><td rowspan="1"><ul><li><a href="#leaves">Leaves</a></li><li><a href="#treetrunk">Tree Trunk</a></li></ul></td><td rowspan="1">Tree</td><td><ul><li>1
<a href="#plantedeggcorn">Planted Eggcorn</a></li></ul></td><td>A natural structure. Grows from a planted eggcorn</td></tr><tr id="treetrunk" class="border-on-top"><td rowspan="1"><ul><li><a href="#log">Log</a></li></ul></td><td rowspan="1">Tree Trunk</td><td><ul><li>1
<a href="#tree">Tree</a></li></ul></td><td>A natural resource. Discover as part of a tree</td></tr><tr id="water" class="border-on-top"><td rowspan="1"><ul><li><a href="#chromaticglass">Chromatic Glass</a></li></ul></td><td rowspan="1">Water</td><td><ul></ul></td><td>A natural resource. Discover as part of a river or sea</td></tr><tr id="woodenadze" class="border-on-top"><td rowspan="1"><ul><li><a href="#peat">Peat</a></li><li><a href="#woodenhatchethead">Wooden Hatchet Head</a></li><li><a href="#woodenmallethead">Wooden Mallet Head</a></li><li><a href="#woodenplank">Wooden Plank</a></li><li><a href="#woodenspadehead">Wooden Spade Head</a></li></ul></td><td rowspan="1">Wooden Adze</td><td><ul><li>1
<a href="#staff">Staff</a></li><li>1
<a href="#stick">Stick</a></li></ul></td><td>Place a stick on the top of a staff</td></tr><tr id="woodenframe" class="border-on-top"><td rowspan="1"><ul><li><a href="#woodenshelf">Wooden Shelf</a></li></ul></td><td rowspan="1">Wooden Frame</td><td><ul><li>2
<a href="#staff">Staff</a></li></ul></td><td>Place a staff on the side of a staff</td></tr><tr id="woodenhatchet" class="border-on-top"><td rowspan="1"><ul><li><a href="#charcoallump">Charcoal Lump</a></li><li><a href="#log">Log</a></li><li><a href="#woodenhatchethead">Wooden Hatchet Head</a></li><li><a href="#woodenmallethead">Wooden Mallet Head</a></li><li><a href="#woodenplank">Wooden Plank</a></li><li><a href="#woodenspadehead">Wooden Spade Head</a></li></ul></td><td rowspan="1">Wooden Hatchet</td><td><ul><li>1
<a href="#staff">Staff</a></li><li>1
<a href="#woodenhatchethead">Wooden Hatchet Head</a></li></ul></td><td>Place a wooden hatchet head on a staff</td></tr><tr id="woodenhatchethead" class="border-on-top"><td rowspan="2"><ul><li><a href="#woodenhatchet">Wooden Hatchet</a></li></ul></td><td rowspan="2">Wooden Hatchet Head</td><td><ul><li>1
<a href="#woodenhatchet">Wooden Hatchet</a></li><li>1
<a href="#woodenspadehead">Wooden Spade Head</a></li></ul></td><td>Pummel a wooden spade head with a hatchet</td></tr><tr><td><ul><li>1
<a href="#woodenadze">Wooden Adze</a></li><li>1
<a href="#woodenspadehead">Wooden Spade Head</a></li></ul></td><td>Pummel a wooden spade head with an adze</td></tr><tr id="woodenmallet" class="border-on-top"><td rowspan="1"><ul><li><a href="#adobe">Adobe</a></li><li><a href="#aggregate">Aggregate</a></li><li><a href="#charcoalblock">Charcoal Block</a></li><li><a href="#dirt">Dirt</a></li><li><a href="#render">Render</a></li></ul></td><td rowspan="1">Wooden Mallet</td><td><ul><li>1
<a href="#staff">Staff</a></li><li>1
<a href="#woodenmallethead">Wooden Mallet Head</a></li></ul></td><td>Place a wooden mallet head on a staff</td></tr><tr id="woodenmallethead" class="border-on-top"><td rowspan="2"><ul><li><a href="#woodenmallet">Wooden Mallet</a></li><li><a href="#woodenspadehead">Wooden Spade Head</a></li></ul></td><td rowspan="2">Wooden Mallet Head</td><td><ul><li>1
<a href="#woodenhatchet">Wooden Hatchet</a></li><li>1
<a href="#woodenplank">Wooden Plank</a></li></ul></td><td>Pummel a wooden plank with a hatchet</td></tr><tr><td><ul><li>1
<a href="#woodenadze">Wooden Adze</a></li><li>1
<a href="#woodenplank">Wooden Plank</a></li></ul></td><td>Pummel a wooden plank with an adze</td></tr><tr id="woodenplank" class="border-on-top"><td rowspan="2"><ul><li><a href="#stick">Stick</a></li><li><a href="#woodenmallethead">Wooden Mallet Head</a></li><li><a href="#woodenshelf">Wooden Shelf</a></li></ul></td><td rowspan="2">Wooden Plank</td><td><ul><li>1
<a href="#log">Log</a></li><li>1
<a href="#woodenhatchet">Wooden Hatchet</a></li></ul></td><td>Pummel a log with a hatchet</td></tr><tr><td><ul><li>1
<a href="#log">Log</a></li><li>1
<a href="#woodenadze">Wooden Adze</a></li></ul></td><td>Pummel a log with an adze</td></tr><tr id="woodenshelf" class="border-on-top"><td rowspan="1"><ul></ul></td><td rowspan="1">Wooden Shelf</td><td><ul><li>4
<a href="#woodenframe">Wooden Frame</a></li><li>1
<a href="#woodenplank">Wooden Plank</a></li></ul></td><td>Place 4 wooden frames in a diamond arrangement, then place a wooden plank in the centre [makes 4]</td></tr><tr id="woodenspade" class="border-on-top"><td rowspan="1"><ul><li><a href="#loosedirt">Loose Dirt</a></li><li><a href="#loosesand">Loose Sand</a></li></ul></td><td rowspan="1">Wooden Spade</td><td><ul><li>1
<a href="#staff">Staff</a></li><li>1
<a href="#woodenspadehead">Wooden Spade Head</a></li></ul></td><td>Place a wooden spade head on a staff</td></tr><tr id="woodenspadehead" class="border-on-top"><td rowspan="2"><ul><li><a href="#woodenhatchethead">Wooden Hatchet Head</a></li><li><a href="#woodenspade">Wooden Spade</a></li></ul></td><td rowspan="2">Wooden Spade Head</td><td><ul><li>1
<a href="#woodenhatchet">Wooden Hatchet</a></li><li>1
<a href="#woodenmallethead">Wooden Mallet Head</a></li></ul></td><td>Pummel a wooden mallet head with a hatchet</td></tr><tr><td><ul><li>1
<a href="#woodenadze">Wooden Adze</a></li><li>1
<a href="#woodenmallethead">Wooden Mallet Head</a></li></ul></td><td>Pummel a wooden mallet head with an adze</td></tr></tbody></table>]]></content><author><name>Joe Koop</name></author><category term="nodecore" /><summary type="html"><![CDATA[Recipe book for the Minetest game Nodecore]]></summary></entry></feed>