Prototype Generation

Prototype Generation Example

Prototype Generation is Primodium's internal process for populating prototype tables with prototype configuration data. This page describing an example workflow used internally for creating a new ore distribution map and writing to a terrain prototype table.

Creating a Terrain Map

Primodium is played on a 2-dimensional map with manually-designed ore generation. The following sprite sheet displays ore tiles used in Primodium, with its corresponding IDs encoded below.

alt text

@primodiumxyz/contracts/ts/terrain/terraingen.ts#L19-L28
const numberBase: Record<string, string> = {
  1: "Copper",
  2: "Iron",
  3: "Lithium",
  4: "Sulfur",
  5: "Titanium",
  6: "Kimberlite",
  7: "Iridium",
  8: "Platinum",
};

Using the open-source Tiled (opens in a new tab) map editor, we first create a csv file that contains the coordinates of the resources. The following is an ore distribution of Iron on a 37 x 25 space rock:

alt text

which is exported into the following .csv file, with commas as separators.

alt text

Populating Tables

The following script creates a Solidity script from the CSV file that encodes the ore distribution. The script reads the CSV file and converts the ore distribution into a Foundry script that sets the ore distribution on the world contract.

@primodiumxyz/contracts/ts/terrain/terraingen.ts
import { formatAndWriteSolidity } from "@latticexyz/common/codegen";
import fs from "fs";
import path from "path";
 
type terrainFile = { id: string; filePath: string };
type JsonCoords = {
  coord: { x: number; y: number };
  index: number;
  value: string;
};
export async function terraingen(
  csvSrcs: terrainFile[],
  outputBaseDirectory: string
) {
  const json = csvToJsonCoords(csvSrcs);
  const content = generateContent(json);
  const finalContent = addContext(content);
  const fullOutputPath = path.join(
    outputBaseDirectory,
    `scripts/CreateTerrain.sol`
  );
  await formatAndWriteSolidity(
    finalContent,
    fullOutputPath,
    `Generated terrain`
  );
}
 
const numberBase: Record<string, string> = {
  1: "Copper",
  2: "Iron",
  3: "Lithium",
  4: "Sulfur",
  5: "Titanium",
  6: "Kimberlite",
  7: "Iridium",
  8: "Platinum",
};
 
function csvToJsonCoords(csvUrls: terrainFile[]) {
  const result: Array<JsonCoords> = [];
 
  csvUrls.forEach((csvUrl) => {
    const csv = fs.readFileSync(csvUrl.filePath, "utf-8");
    const lines = csv.split("\n");
    for (let i = 0; i < lines.length; i++) {
      const currentLine = lines[i]
        .replace(/\s+/g, "")
        .split(",")
        .filter((x) => !!x);
      for (let j = 0; j < currentLine.length; j++) {
        if (currentLine[j] == "-1") continue;
        const value = numberBase[currentLine[j]];
        if (!value)
          throw new Error(
            `Invalid value ${currentLine[j]} at line ${i}, column ${j}`
          );
        result.push({
          coord: { x: j, y: i },
          index: Number(csvUrl.id),
          value: value,
        });
      }
    }
  });
 
  return result;
}
 
function generateContent(jsonContent: JsonCoords[]) {
  return jsonContent
    .map(
      (elem) =>
        `P_Terrain.set(${elem.index}, ${elem.coord.x}, ${elem.coord.y}, uint8(EResource.${elem.value}));`
    )
    .join("");
}
 
function addContext(str: string) {
  return `// SPDX-License-Identifier: MIT
pragma solidity >=0.8.21;
 
import { P_Terrain } from "codegen/index.sol";
import { EResource } from "codegen/common.sol";
 
  function createTerrain() {
    ${str}
}
`;
}

The generated Foundry script populates the P_Terrain table with the ore distribution, which is a prototype table. The following is the generated script for the Iron ore distribution:

@primodiumxyz/contracts/src/codegen/scripts/CreateTerrain.sol
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.24;
 
import { P_Terrain } from "codegen/index.sol";
import { EResource } from "codegen/common.sol";
 
function createTerrain() {
  P_Terrain.set(1, 17, 4, uint8(EResource.Iron));
  P_Terrain.set(1, 16, 5, uint8(EResource.Iron));
  // continued with all other iron tiles
}

In the Primodium contract PostDeploy step as described in PostDeploy.s.sol, the createTerrain function is called to populate the P_Terrain table with the ore distribution.

contract PostDeploy is Script {
  function run(address worldAddress) external {
    createTerrain();
    vm.stopBroadcast();
  }
}