Skip to content

Forge Port Guide

How to express a schema-based object (region, area, creature, vehicle, quest) as a forge-backed kind. This guide uses plant_region as the reference implementation; code paths point to the committed port.

Note: This is a living document. The forge-runner extraction plan (plan-forge-runner.md) will extend it once generic host concerns are separated from plant_world.


Lifecycle Overview

Schema spec
    |
    v
ForgeGraph      (graph.rs)       -- nodes + edges describing the kind
    |
    v
run_generators  (generators.rs)  -- JTL generators populate ForgeHost buckets
    |
    v
compile_forge   (compiler.rs)    -- topological compile, assigns genomes
    |
    v
ForgeArtifacts                   -- compiled nodes, links, chem_state, provenance
    |
    v  (domain-specific projection step)
Kind artifacts  (e.g. CompiledRegionArtifacts)
    |
    v
ForgeRegistry   (registry.rs)    -- Pending -> Generating -> Compiled -> Registered -> Active
    |
    v
ForgeService    (service.rs)     -- ForgeService adapter; drives registry through frame phases

The domain-specific projection step is the only phase that knows about kind-specific types. Everything else is generic forge machinery.


Seam Types

ForgeSeedLike

// forge/src/compiler.rs
pub trait ForgeSeedLike: Copy + Send + Sync {
    fn base_seed(&self) -> u64;
}

Implement this on your seed type. The base seed drives deterministic genome assignment across all nodes in the graph.

Region reference: region/src/seed.rsimpl ForgeSeedLike for RegionSeed

ForgeGraph

// forge/src/graph.rs
pub struct ForgeGraph { pub nodes: Vec<ForgeNode>, pub edges: Vec<ForgeEdge> }
pub struct ForgeNode  { pub node_id: String, pub kind: ForgeNodeKind, pub params: BTreeMap<String,Value> }
pub struct ForgeEdge  { pub from: String, pub to: String, pub kind: ForgeEdgeKind }

Build one from your spec type. The conversion belongs on the spec type as a to_forge_graph(&self) -> ForgeGraph method.

Region reference: region/src/graph_spec.rsRegionGraphSpec::to_forge_graph()

ForgeExtension

// forge/src/compiler.rs
pub trait ForgeExtension<S: ForgeSeedLike> {
    fn on_node(&self, node: &mut CompiledForgeNode, seed: S);
    fn on_link(&self, link: &mut ForgeLink, seed: S);
}

Optional. Pass None if no per-node overrides are needed. Region currently passes None; extension hooks were kept for future use.

ForgeHost

The JTL host used during generator execution. Create with:

ForgeHost::new(genome_hex, trait_map)

After run_generators(program, host) returns, extract named buckets:

let config_bucket = host.take_bucket("config");
let graph_bucket  = host.take_bucket("graph");

Region reference: region/src/generators.rsrun_region_generators()

ForgeRegistry / ForgeRecord

Generic over TCompiled (your kind's compiled artifact type). States advance linearly: Pending -> Generating -> Compiled -> Registered -> Active.

Bulk advance via:

registry.advance_all_from_state(ForgeLifecycleState::Generating);

ForgeService

Wraps ForgeRegistry<TCompiled> and implements ForgeService. Frame hooks drive state transitions automatically:

Frame phase Transition
on_init Pending -> Generating
on_active Generating -> Compiled
on_aggregate Compiled -> Registered
on_cleanup Registered -> Active

Porting a New Kind: Step-by-Step

1. Add plant_forge dependency

# your-crate/Cargo.toml
[dependencies]
plant_forge = { workspace = true }

2. Implement ForgeSeedLike for your seed

impl plant_forge::ForgeSeedLike for YourSeed {
    fn base_seed(&self) -> u64 {
        self.primary_seed ^ self.secondary_seed.rotate_left(21)
    }
}

3. Add to_forge_graph() to your spec type

Map your spec nodes/edges into ForgeNode / ForgeEdge. Node kinds that have no direct forge equivalent use ForgeNodeKind::Generic.

4. Write a projection function

// your-crate/src/compiler/projection.rs
pub fn project_forge_artifacts(
    spec: &YourSpec,
    seed: YourSeed,
    artifacts: ForgeArtifacts,
) -> Result<YourArtifacts, String> {
    // Map CompiledForgeNode -> your domain type.
    // Copy over chem_state, provenance, dirty, warnings.
}

Keeping the projection in its own file makes the seam visible and testable.

Region reference: region/src/compiler/projection.rs

5. Delegate compile and generators

// compile path
let forge_graph = spec.to_forge_graph();
let forge_artifacts = compile_forge(&your_id, &forge_graph, seed, None)?;
let your_artifacts = project_forge_artifacts(&spec, seed, forge_artifacts)?;

// generator path
let host = ForgeHost::new(genome_hex, trait_map);
let host = run_generators(program, host);

Region references:

  • region/src/compiler/compile.rscompile_region_inner()
  • region/src/generators.rsrun_region_generators()

6. Register with ForgeService (when forge_runner is available)

let service: ForgeService<YourArtifacts> = ForgeService::new("your_kind".to_string());
let registry = service.registry(); // Arc<RwLock<ForgeRegistry<YourArtifacts>>>
frame_manager.register_service(service);

Naming Conventions

Concept Name pattern
Seed type YourKindSeed
Spec -> graph conversion YourKindSpec::to_forge_graph
Forge -> kind artifact mapping project_forge_artifacts
Generator host wrapper YourKindGeneratorHost
Registry ForgeRegistry<YourArtifacts>
Service ForgeService<YourArtifacts>

Source Map

File Role
forge/src/graph.rs ForgeGraph, ForgeNode, ForgeEdge
forge/src/compiler.rs compile_forge, ForgeSeedLike, ForgeArtifacts
forge/src/host.rs ForgeHost, ForgeEvent
forge/src/generators.rs run_generators
forge/src/provenance.rs ProvenanceLog, CompileEvent
forge/src/dirty.rs DirtyFlags
forge/src/registry.rs ForgeRegistry, ForgeRecord, ForgeLifecycleState
forge/src/service.rs ForgeService (ForgeService adapter)
region/src/seed.rs ForgeSeedLike impl (reference)
region/src/graph_spec.rs to_forge_graph() (reference)
region/src/compiler/compile.rs compile path delegation (reference)
region/src/compiler/projection.rs project_forge_artifacts (reference)
region/src/generators.rs ForgeHost + run_generators (reference)