Skip to content
TAURION
09Services & Crafting

Services & Crafting

Refining, repair, reverse engineering, blueprint copying and construction.

For players (plain language): Buildings in Taurion are workshops. Bring raw materials or blueprints to one and you can do five things: refine ore into usable materials (e.g. Trimideum raw a → Agarite mat a + Borolium mat b), repair a vehicle's armour, reverse-engineer ancient artefacts into blueprints, copy a reusable master blueprint into single-use copies, and build (construct) fitments and vehicles from a blueprint plus materials. Every job costs the in-game coin vCHI; that "base cost" is destroyed (burnt) forever. If the building belongs to another player, you also pay a service fee to its owner on top (the old "ancient" buildings that came with the map charge no fee). Some jobs finish instantly; repair, copying and construction take several blocks (turns) to complete, during which your vehicle or blueprint is tied up. The rest of this page gives the exact rules and numbers; player-relevant display names (Trimideum, Looter, ...) are shown next to the internal item codes (raw a, rv s, ...) throughout, with a full lookup table in section 11.

Authoritative reference for every building-service / crafting operation in Taurion. Single source of truth: the Game State Processor (GSP). All rules, formulas, and constants below are cited as file:line from the GSP source tree.

The service subsystem lives almost entirely in src/services.cpp and src/services.hpp, with completion of multi-block operations driven by src/ongoings.cpp. The numeric parameters come from the read-only config (roconfig); see proto/config.proto and proto/roconfig/params.pb.text.

Currency note. Throughout this document, "vCHI" is the in-game fungible coin (the GSP type Amount, an integer count of indivisible coins). The two kinds of payment for a service are the base cost (always burnt / removed from supply) and the service fee (paid to the building owner). See Cost model below.


0. The five service operations at a glance#

There are five service operation types, all subclasses of ServiceOperation (the abstract base class is declared in services.hpp:48; its shared logic — cost, fee, execution — lives in services.cpp:969-1145). Each is requested with a short move code. A blueprint is a craftable recipe item: a bpo ("original", re-usable master) or a bpc ("copy", single-use). A fitment is an equippable module (weapon, armour plate, refinery, etc.) installed on a vehicle.

Move t Operation Subclass Where output goes Multi-block? Ongoing proto
ref Refining RefiningOperation (services.cpp:68) building/character inventory No (instant)
fix Armour repair RepairOperation (services.cpp:273) the character Yes armour_repair
rve Reverse engineering RevEngOperation (services.cpp:444) building inventory No (instant)
cp Blueprint copy (bpo → bpc) BlueprintCopyOperation (services.cpp:601) building inventory Yes blueprint_copy
bld Construction of item/vehicle ConstructionOperation (services.cpp:763) building inventory Yes item_construction

Dispatch by code happens in ServiceOperation::Parse (services.cpp:1217-1233).

Refining additionally has a mobile-refinery variant performed by a character out in the world (not in a building). It is parsed by a separate entry point, ServiceOperation::ParseMobileRefining (services.cpp:1245), and reuses RefiningOperation.


1. Cost model (base cost, service fee, burns)#

Every service has a base cost and possibly a service fee. They are computed together in ServiceOperation::GetCosts (services.cpp:986-1016):

base = GetBaseCost()         // virtual; defined per operation (see each section)

The base cost is always burnt (subtracted from the requesting account, never credited anywhere) in ServiceOperation::Execute (services.cpp:1127-1145):

acc.AddBalance (-base - fee);
if (fee > 0) owner->AddBalance (fee);   // fee credited to building owner

Service fee rules (services.cpp:991-1016)#

The fee is zero in any of these cases:

  1. The operation is not inside a building (mobile refinery has no fee) — building == nullptr (services.cpp:993-997).
  2. The building is an ANCIENT building (building->GetFaction () == Faction::ANCIENT) (services.cpp:1004). All the initial map buildings are ancient, so all services in them are fee-free (base cost still burns).
  3. The requesting account owns the building (building->GetOwner () == acc.GetName ()) (services.cpp:1005). This explicit zeroing matters because it lets an owner run services on a "tight budget" that would not allow temporarily fronting the fee (services.cpp:999-1003).

Otherwise (player-owned building, used by someone else):

fee = ceil(base * service_fee_percent / 100)
    = (base * service_fee_percent + 99) / 100        // services.cpp:1015
  • service_fee_percent is a per-building owner-controlled config value (proto/building.proto:88, Building.Config.service_fee_percent).
  • The result is rounded up (integer ceiling). Confirmed by ServicesFeeTests.FeeRoundedUp (services_tests.cpp:371-383): refining 3 test ore = 1 step → base 10, fee 1% → ceil(10·1/100) = ceil(0.1) = 1 (andy ends on 11, domob on 89).
  • A 0% fee is legal: ServicesFeeTests.ZeroFeePossible (services_tests.cpp:357-369).
  • Maximum settable fee: MAX_SERVICE_FEE_PERCENT = 1000 (i.e. 1000%, a 10× multiplier) — moveprocessor.cpp:48. Any building-config move setting a higher value is rejected (moveprocessor.cpp:265). This cap is consensus relevant and is intended to disallow "completely scam" buildings.
  • Changing a building's service_fee_percent is itself delayed (anti-front-running): the new config takes effect after building_update_delay blocks (mainnet 120, regtest 10) via a building_update ongoing operation (ongoings.cpp:206-218, params.pb.text:18, test_params.pb.text).

Affordability check (services.cpp:1091-1100)#

IsFullyValid() requires base + fee <= acc.GetBalance(). If the account can't cover both, the whole operation is invalid and nothing happens. ServicesFeeTests.InsufficientBalanceWithFee shows a user with exactly the base cost but no fee budget being rejected (services_tests.cpp:324-341).

Worked fee example (from tests)#

ServicesFeeTests.NormalFeePayment (services_tests.cpp:343-355): domob refines 3 test ore (1 step, base cost 10) in andy's building with 50% fee.

  • base = 10 (burnt), fee = ceil(10·50/100) = 5.
  • domob: 100 → 85 (paid 15 total). andy (owner): 10 → 15 (received the 5 fee).

The PendingJson test (services_tests.cpp:244-268) shows the cost breakdown surfaced to the UI: base 20, fee 10 for a 6-ore (2-step) refine at 50%.


2. The move envelope & RPC#

2.1 Move JSON for building services (s array)#

Building services are submitted as an array under key s in the player's move. Dispatch: BaseMoveProcessor::TryServiceOperations (moveprocessor.cpp:425-446). Each array element is one operation object parsed by ServiceOperation::Parse.

{
  "s": [
    { "t": "ref", "b": 100, "i": "raw a", "n": 10000 },
    { "t": "fix", "b": 100, "c": 200 }
  ]
}

Common required fields of every s entry:

Field Type Meaning Source
t string operation code (ref/fix/rve/cp/bld) services.cpp:1207-1213
b int (>0) building ID the operation happens in services.cpp:1185-1190

Building validation in Parse:

  • The building must exist (services.cpp:1192-1199).
  • The building must not be a foundationb->GetProto ().foundation () is rejected (services.cpp:1200-1205). Services only work in finished buildings.
  • The building must offer the requested service (per-type offered_services, checked in IsFullyValidIsSupported(building), services.cpp:1075-1082).

The per-operation extra fields (i, n, c) are documented in each section below. Note the strict object-size checks: most ops require exactly the expected number of keys, so adding an extra field invalidates the move (e.g. ParseItemAmount requires data.size () == 4, services.cpp:1154; repair requires size () == 3, services.cpp:427). Tests *.InvalidFormat exercise an "x": false extra field being rejected (services_tests.cpp:389-397 etc.).

2.2 The (i, n) item-amount shape#

Refining, reverse-engineering, blueprint-copy and construction all share the ParseItemAmount helper (services.cpp:1147-1167):

Field Type Meaning
i string item type (ore / artefact / blueprint type)
n int amount / count

n is parsed by QuantityFromJson (jsonutils.cpp:137-145) which requires a plain positive integer in (0, MAX_QUANTITY], MAX_QUANTITY = 2^50 (database/inventory.hpp:54). Floats (3.0), strings ("6"), zero, and negatives are all rejected (services_tests.cpp:404-421).

2.3 Move JSON for the mobile refinery (inside a character update)#

Mobile refining is not in the s array. It is a sub-key ref inside a per-character update object. Dispatch: BaseMoveProcessor::TryMobileRefining (moveprocessor.cpp:407-423), called from character processing (moveprocessor.cpp:1883).

{
  "c": {
    "200": {
      "ref": { "i": "raw a", "n": 30000 }
    }
  }
}

The ref object has exactly 2 keys i and n (services.cpp:1254) — no b, no t (the building and type are implicit). The character is the one in the update.

Ordering note: mobile refining is processed before drop/pickup in a character move, so a single move can refine then drop the refined output or free cargo for a pickup (moveprocessor.cpp:1880-1889).

2.4 RPC: getserviceinfo#

Clients can preview cost/validity without sending a move via the getserviceinfo(name, op) RPC (pxrpcserver.cpp:627-664). It takes the same op JSON as an s entry, parses at height + 1, and returns the operation's pending JSON (see per-op "pending JSON" below) plus an extra boolean field "valid" = IsFullyValid(). Returns JSON null if the op can't even be parsed, and raises an INVALID_ACCOUNT RPC error if name does not exist (pxrpcserver.cpp:644-650).

Note: even an invalid op (e.g. armour at full HP, so no missing HP) still returns a computed cost — for repair this is 0 (services.cpp:384-388).


3. Building services availability#

Whether a building offers a service is part of its read-only type config (BuildingData.Services, proto/config.proto:358-369). The defaults are false, so only the offered services are listed in each building's .pb.text.

The required service per operation:

Operation Required offered_services flag Check
ref refining refining services.cpp:116-120
fix armour repair armour_repair services.cpp:294-299
rve reverse engineering reverse_engineering services.cpp:463-468
cp blueprint copy blueprint_copy services.cpp:623-628
bld construct fitment/item item_construction services.cpp:833-843
bld construct vehicle vehicle_construction services.cpp:833-843

The construction operation picks the required flag at runtime based on whether the output item has a vehicle config (→ vehicle_construction) or not (→ item_construction), services.cpp:839-842. ConstructionTests.RequiredServiceType (services_tests.cpp:1363-1405) confirms an item_construction-only building (itemmaker) can build a sword but not a chariot vehicle, and vice-versa for a vehicle_construction-only carmaker.

Which building types offer what (mainnet roconfig)#

From proto/roconfig/buildings/:

Building type refining armour_repair reverse_eng bp_copy item_constr vehicle_constr
ancient1 / ancient2 / ancient3
*_cc (combined complex)
*_ss (space station)
*_r (refinery)
*_rf
*_vb (vehicle bay)
*_cf (construction facility)
*_c1*_c11 (per-faction craft) varies varies varies varies varies
*_rt, obelisk1/2/3 (none — offered_services: {})

(Example: r_c1 = armour_repair + vehicle_construction; r_c2 = armour_repair + refining. Each _cN always pairs armour_repair with exactly one production service, and the pairing differs by faction and index — see note below.)

The *_c1*_c11 "craft" buildings each offer armour_repair plus one production service in varying combinations; consult the individual <faction>_c<N>.pb.text for the exact pair. The r_/g_/b_ prefix is the faction (red/green/blue) and only matters for who may construct the building, not for service eligibility. Obelisks and *_rt buildings offer no services.

Ancient buildings always charge no service fee (section 1) — the initial map buildings are all ancient.


4. Refining (ref)#

Turns raw ore into refined materials. Class RefiningOperation (services.cpp:68-266).

Move#

{ "t": "ref", "b": 100, "i": "raw a", "n": 10000 }
  • i — an item type whose config has a refines block (RefiningData, proto/config.proto:48-63). Otherwise invalid (services.cpp:176-195).
  • n — amount of input ore to consume. Must be a positive multiple of the effective input units per step (services.cpp:206-212).

Refining model#

Each refinable ore defines (RefiningData):

Field Meaning
input_units input ore consumed per one refining "step"
cost base vCHI burnt per step
outputs (map) item→qty produced per step

Number of steps = n / effective_input_units (services.cpp:107-111). The amount must divide evenly, else the move is invalid (services.cpp:206-212; RefiningTests.InvalidAmount rejects n:2 for a 3-unit step, services_tests.cpp:447-467).

  • Base cost = steps × cost (services.cpp:128-132).
  • Output = for each output type, steps × per_step_amount (services.cpp:248-266).
  • Execution: subtract all input first, then add outputs (services.cpp:260-265). The comment notes refining always reduces cargo volume, so a mobile refinery can never overflow cargo this way (services.cpp:256-258).

Test-config example (test ore, items/test.pb.text:22-36)#

test ore: input_units: 3, cost: 10, outputs {bar: 2, zerospace: 1}.

  • Refine n:3 → 1 step → base 10, +2 bar, +1 zerospace (ServicesTests.BasicOperation, services_tests.cpp:129-143).
  • Refine n:9 → 3 steps → base 30, +6 bar, +3 zerospace (RefiningTests.MultipleSteps, services_tests.cpp:479-493).

Real-config example (raw a = "Trimideum")#

items/materials.pb.text:61-75: raw a has input_units: 10000, cost: 1, outputs {mat a: 1000, mat b: 300}. So refining 10000 raw a (Trimideum) → 1 step → burns 1 vCHI → produces 1000 mat a (Agarite) + 300 mat b (Borolium). All 9 ores (raw araw i) use input_units: 10000 and cost: 1; only the output mix differs.

Pending JSON (SpecificToPendingJson, services.cpp:227-246)#

{
  "type": "refining",
  "input":  { "test ore": 6 },
  "output": { "bar": 4, "zerospace": 2 }
}

(RefiningTests.PendingJson, services_tests.cpp:495-507.)

Mobile refinery variant#

A character carrying a mobile-refinery fitment can refine in the field. Supported if the character proto has_refining() (services.cpp:122-126). The on-character MobileRefinery config carries an input inefficiency modifier (character.proto:60-71, proto/config.proto:215).

  • The modifier is applied to input_units only: InputUnitsPerStep = inputModifier(refData->input_units()) (services.cpp:97-101, services.cpp:166-174).
  • Cost per step and outputs per step are unchanged — only the ore burned per step increases (proto doc, character.proto:63-69).
  • No service fee (it isn't in a building), but the base cost still burns (services.cpp:993-997).
  • The refined output goes into the character's inventory (services.cpp:1040-1049).

The mobile-refinery fitment in real config is vhf refinery ("Mobile Refinery" in the game UI) with refining: { input: { percent: 100 } } (items/fitments.pb.text:2620). A +100% modifier doubles the input units per step.

Worked example, MobileRefiningTests (services_tests.cpp:533-541, modifier percent: 100): test ore base step is 3 → effective 6. Refining n:18 → 3 steps → base 30, output {bar: 6, zerospace: 3} (MobileRefiningTests.MultipleSteps, services_tests.cpp:597-608). Note 18 input → only 6 bar (vs. 12 bar a stationary refinery would yield from 18 ore), illustrating the inefficiency.

Pending JSON for mobile refining adds "building": null and "character": <id> and cost.fee is always 0 (MobileRefiningTests.PendingJson, services_tests.cpp:669-687).


5. Armour repair (fix)#

Restores a character's armour HP. Class RepairOperation (services.cpp:273-437). This is the only multi-block operation that targets a character rather than a building inventory.

Move#

{ "t": "fix", "b": 100, "c": 200 }
  • c — the character ID to repair (services.cpp:430-436). Move must have exactly 3 keys (services.cpp:427).

Validity (services.cpp:331-374)#

  • The character must exist (services.cpp:334-338).
  • The requester must own the character (services.cpp:340-346).
  • The character must be inside this exact building (ch->IsInBuilding() && ch->GetBuildingId() == building.id, services.cpp:348-355).
  • The character must not be busy (no other ongoing op) — ch->IsBusy() (services.cpp:357-362). RepairTests.AlreadyRepairing confirms you can't stack repairs (services_tests.cpp:834-854).
  • There must be missing armour HP (maxArmour - curArmour > 0, services.cpp:364-371). RepairTests.NoMissingHp rejects full-armour repair (services_tests.cpp:769-777).

GetMissingHp = max_hp.armour - hp.armour (services.cpp:284-290). Only armour is repaired; shields regenerate naturally and are not part of this service.

Cost formula (services.cpp:376-391)#

costMillis = missingHp × armour_repair_cost_millis
base       = ceil(costMillis / 1000) = (costMillis + 999) / 1000
fee        = standard service-fee rule (section 1)
  • armour_repair_cost_millis is the cost in 1/1000 vCHI per HP. Mainnet/testnet value: 100 (= 1 vCHI per 10 HP) (params.pb.text:12). Not overridden in regtest, so 100 everywhere.
  • Cost is rounded up to whole vCHI.

Duration / completion (services.cpp:403-418)#

When executed, repair does not restore HP immediately. It creates an ongoing operation that ends after:

hpPerBlock = armour_repair_hp_per_block        // mainnet/testnet: 100, params.pb.text:11
blocksBusy = ceil(missingHp / hpPerBlock) = (missingHp + hpPerBlock - 1) / hpPerBlock
op.height  = currentHeight + blocksBusy

The character is marked busy: ch->MutableProto().set_ongoing(op.id) and the ongoing carries armour_repair plus the character ID (services.cpp:413-417). HP is not changed at start (RepairTests.BasicExecution asserts armour stays 950, services_tests.cpp:779-800).

Completion (ongoings.cpp:184-189): at the target height, armour is set fully to max in one shot and the character's ongoing flag cleared:

c->MutableHP().set_armour(c->GetRegenData().max_hp().armour());
c->MutableProto().clear_ongoing();

So the entire missing amount is restored at the end regardless of hpPerBlock; hpPerBlock only sets how many blocks it takes.

Worked examples (test config, max armour 1000)#

Missing HP base cost (vCHI) blocks busy end height (start 100) Test
50 (950→1000) ceil(50·100/1000)=5 ceil(50/100)=1 101 BasicExecution (services_tests.cpp:779-800)
1 (999→1000) ceil(1·100/1000)=1 1 101 SingleHpMissing (services_tests.cpp:802-816)
900 (100→1000) ceil(900·100/1000)=90 ceil(900/100)=9 109 MultipleBlocks (services_tests.cpp:818-832)

Pending JSON (services.cpp:393-401)#

{ "type": "armourrepair", "character": 200 }

(RepairTests.PendingJson, services_tests.cpp:856-866.)


6. Reverse engineering (rve)#

Consumes artefacts to (probabilistically) yield original blueprints (bpo). Class RevEngOperation (services.cpp:444-594). This is the only service whose output is random (uses xaya::Random).

Move#

{ "t": "rve", "b": 100, "i": "test artefact", "n": 1 }
  • i — an item whose config has a reveng block (RevEngData, proto/config.proto:97-109). Otherwise invalid (services.cpp:494-509).
  • n — number of artefacts to reverse-engineer (each is one independent "trial").

Cost (services.cpp:470-474)#

base = n × reveng.cost      // burnt
fee  = standard rule

reveng.cost for test artefact is 10 (items/test.pb.text:175); for the real artefacts (art c/art uc/art r/art ur) it is 1 (items/artefacts.pb.text). The cost is paid up front for all n attempts regardless of how many succeed.

Execution & odds (services.cpp:547-594)#

  1. Remove all n input artefacts up front (services.cpp:554-555).
  2. Build the eligible output list from reveng.possible_outputs, filtered to only items that are faction-neutral or match the requesting account's faction (services.cpp:560-573). Faction is read from the account.
  3. For each of the n trials independently (services.cpp:575-593):
    • Uniformly pick one candidate output type from the eligible list (rnd.NextInt, services.cpp:577).
    • Look up how many of that exact output type have already been found game-wide via ItemCounts.GetFound (services.cpp:580, database/itemcounts.hpp:53).
    • Compute the success chance as 1 / N where N = Params::RevEngSuccessChance(existingCount) (params.cpp:51-92).
    • Roll rnd.ProbabilityRoll(1, N). On success: add 1 of that blueprint to the building inventory and increment its global found-counter (services.cpp:588-592).

Crucially: a failed roll yields nothing (the artefact and the vCHI for that trial are gone). Picking a candidate then failing the probability roll is a real possible outcome — the candidate is chosen first, then the per-type diminishing odds are applied.

Diminishing-returns chance formula (params.cpp:51-92)#

base_N  = baseChance        // 10 on MAIN/TEST/POLYGON/MUMBAI; 1 on REGTEST/GANACHE
for each already-found blueprint of this type:
    base_N = base_N × 4 / 3            // i.e. ÷75% per existing copy → rarer
    if base_N ≥ 1,000,000,000: return 1,000,000,000   (floor of 1/1e9)
return base_N

(Computed in fixed-point internally to avoid rounding to <1 on regtest; fpMultiple = 1e6, minChance = 1e9.)

So on mainnet the first copy of a given blueprint type succeeds at 1/10; each subsequent existing copy multiplies the denominator by 4/3, making each next find progressively rarer (1/10 → 1/13 → 1/17 → …), capped at a 1-in-1,000,000,000 floor. This makes blueprints that nobody has yet far easier to discover than ones already widely held — a deflationary supply mechanic.

Existing found (this type) Mainnet N (≈) Chance
0 10 1/10
1 13 1/13
2 17 1/17
5 42 1/42
10 177 1/177

Faction restriction (test, services_tests.cpp:1015-1061)#

test artefact possible outputs are bow bpo, sword bpo, red fitment bpo (items/test.pb.text:169-183). red fitment is faction r. A GREEN account reverse-engineering it never gets red fitment bpo (it is filtered from the candidate list), and the code is careful to consume exactly one random pick + one probability roll per trial even when an item would be ineligible — there are no hidden re-rolls (FactionRestriction).

Worked example (RevEngTests.OneItem, services_tests.cpp:961-980)#

Reverse-engineer 1 test artefact: base cost 10 burnt; you end up with the artefact count reduced by 1 and 0 or 1 blueprint added (sum of bow/sword/red == 1 only if the roll succeeds — the test asserts the found counters track whatever was produced).

Pending JSON (services.cpp:534-545)#

{ "type": "reveng", "input": { "test artefact": 2 } }

(No output is shown — it's random; RevEngTests.PendingJson, services_tests.cpp:1063-1074.)


7. Blueprint copying (cp, bpo → bpc)#

Makes single-use copies (bpc) from a re-usable original (bpo). Class BlueprintCopyOperation (services.cpp:601-735).

Blueprint item naming (proto/roconfig.cpp:248-344)#

Blueprints are auto-generated item types for any base item with with_blueprint: true:

  • "<item> bpo" = original blueprint (BlueprintData.original = true). Re-usable; can be copied.
  • "<item> bpc" = copy (original = false). Single-use; cannot be copied again.

Both have space: 1 (roconfig.cpp:256-257) and inherit the base item's faction (roconfig.cpp:327-328). Example: sword bpo, sword bpc, rv s bpo, etc.

Move#

{ "t": "cp", "b": 100, "i": "sword bpo", "n": 10 }
  • i — must be a bpo (an original). Constructor rejects non-blueprints, non-originals (a bpc), and unknown items (services.cpp:649-674; BlueprintCopyTests.InvalidItemType rejects sword and sword bpc, services_tests.cpp:1124-1144).
  • n — number of copies to produce (> 0).

Validity (services.cpp:676-697)#

You must own at least one of the original in the building inventory (balance == 0 → invalid). Note only one original is needed regardless of n (an original is re-usable and is locked away during copying, then returned).

Cost (services.cpp:630-635)#

complexity = complexity of the base item (e.g. sword=1, bow=100)   // services.cpp:671
base = n × (bp_copy_cost × complexity)        // burnt
fee  = standard rule

bp_copy_cost is vCHI per complexity per copy:

Network bp_copy_cost bp_copy_blocks Source
Mainnet/testnet 1 1 params.pb.text:13-14
Regtest 100 10 test_params.pb.text

Note: bp_copy_blocks is defined in config but the duration formula actually uses construction_blocks, not bp_copy_blocks — see below. bp_copy_blocks appears unused by the copy timing code.

Duration / completion (per-copy serial production)#

Execution (services.cpp:713-735):

  1. Remove 1 original from the inventory immediately (inv.AddFungibleCount(original, -1), services.cpp:721). The original is "locked" for the whole job.
  2. Create an ongoing blueprint_copy with num_copies = n, scheduled after the base duration of ONE copy (not the whole batch):
    baseDuration = GetBpCopyBlocks(copyType) = construction_blocks × baseComplexity   // services.cpp:739-751
    op.height = currentHeight + baseDuration
    

Each completion tick (ongoings.cpp:43-67, UpdateBlueprintCopy):

  • Adds 1 finished bpc to the account's building inventory.
  • If copies remain (num_copies − 1 > 0): decrement and reschedule another baseDuration blocks ahead (copies are produced one at a time, serially).
  • When the last copy completes: also return the original bpo to the inventory (ongoings.cpp:65-66).

So the total time for n copies of complexity-X item is n × construction_blocks × X blocks, and the original is only returned at the very end. Copies are not "in" the inventory until each finishes.

Worked example (BlueprintCopyTests.Success, services_tests.cpp:1172-1197)#

Start height 100; copy sword bpo (sword complexity 1) n:10 on regtest (construction_blocks = 10). Result:

  • base cost = 10 × (bp_copy_cost 100 × complexity 1) = 1000 vCHI burnt (domob 1,000,000 → 999,000).
  • The original is removed; no sword bpo or sword bpc is in inventory yet (both 0 at start of the job).
  • Ongoing blueprint_copy created with account=domob, original_type="sword bpo", copy_type="sword bpc", num_copies=10, first tick at height 100 + 10 = 110 (= GetBpCopyBlocks = 10×1).

Pending JSON (services.cpp:699-711)#

{
  "type": "bpcopy",
  "original": "sword bpo",
  "output": { "sword bpc": 2 }
}

(BlueprintCopyTests.PendingJson, services_tests.cpp:1199-1211.)


8. Construction (bld) — items & vehicles from blueprints#

Builds finished fitments/items or vehicles from a blueprint + resources. Class ConstructionOperation (services.cpp:763-951). Vehicles and fitments use the same operation; only the required building service differs (section 3).

Move#

{ "t": "bld", "b": 100, "i": "sword bpo", "n": 5 }
  • i — a blueprint, either an original (bpo) or a copy (bpc). The output is the blueprint's for_item (services.cpp:817-831). Passing a non-blueprint (e.g. sword, an invalid item) is rejected (services_tests.cpp:1264-1278).
  • n — number of items to construct.

Blueprint consumption rule (services.cpp:885-934)#

Source blueprint Blueprints needed for n items Blueprint after job
original (bpo) 1 returned when job fully completes
copy (bpc) n (one consumed per item) all n consumed (single-use)

fromOriginal is set from is_blueprint().original() (services.cpp:825). The required-blueprint check: bpRequired = (fromOriginal ? 1 : n) (services.cpp:886-898).

Resources & faction validity (services.cpp:845-901)#

  • If the output item has a faction, the requesting account's faction must match (services.cpp:854-867). ConstructionTests.FactionRestrictions: a GREEN account cannot build red fitment from red fitment bpo, a RED account can (services_tests.cpp:1334-1361).
  • For every entry in the output's construction_resources map, the building inventory must hold n × per_item_amount (services.cpp:869-883). Missing resources → invalid (ConstructionTests.MissingResources, services_tests.cpp:1296-1310).

Cost (services.cpp:790-796)#

base = n × (construction_cost × output_complexity)     // burnt
fee  = standard rule

construction_cost is vCHI per complexity per item:

Network construction_cost construction_blocks Source
Mainnet/testnet 1 1 params.pb.text:15-16
Regtest 100 10 test_params.pb.text

Resources are not part of the vCHI cost — they are consumed separately from inventory. Only construction_cost × complexity × n vCHI is burnt.

Execution (services.cpp:917-951)#

  1. Subtract all n × resource amounts from the building inventory (services.cpp:924-929).
  2. Subtract blueprints: 1 if from original, else n (services.cpp:931-934).
  3. Create an ongoing item_construction scheduled after the time for one item: op.height = currentHeight + GetConstructionBlocks(output) where GetConstructionBlocks = construction_blocks × complexity (services.cpp:936-951, services.cpp:955-960). The ongoing stores account, output_type, num_items, and (only if from original) original_type.

Completion model (ongoings.cpp:69-108, UpdateItemConstruction)#

Two different completion behaviours depending on source (proto doc, proto/ongoing.proto:74-111):

  • From blueprint copies (!has_original_type): all n items are produced in parallel, finished together in a single tick after one item-duration. finished = num_items, all given out at once, no reschedule (ongoings.cpp:82-105).
  • From an original (has_original_type): items are produced serially, one per tick. Each tick gives out 1 item, decrements num_items, and reschedules one item-duration ahead. After the last item, the original blueprint is returned to the inventory (ongoings.cpp:106-107).

So total time:

  • from copies: construction_blocks × complexity blocks (one batch).
  • from original: n × construction_blocks × complexity blocks (serial), and the bpo comes back only at the very end.

Output destination#

Constructed items go into the account's inventory inside the building (ongoings.cpp:77, 96). There is no automatic spawning of a usable vehicle; the built vehicle item sits in the building inventory until the player uses it.

Worked examples (test config, sword complexity 1, regtest blocks 10)#

ConstructionTests setup (services_tests.cpp:1215-1232): domob has sword bpo, sword bpc, 100 zerospace; sword needs construction_resources {zerospace: 10}.

From original (FromOriginal, services_tests.cpp:1407-1433), n:5:

  • base cost = 5 × (100 × 1) = 500 (1,000,000 → 999,500).
  • consumed: sword bpo (1), zerospace 50 (100 → 50). sword bpc untouched (1).
  • ongoing item_construction: account=domob, output=sword, num_items=5, original_type=sword bpo, first tick at 100 + 10 = 110.

From copies (FromCopy, services_tests.cpp:1435-1463), n:5 (needs 5 sword bpc):

  • base cost = 500 (same).
  • consumed: 5 sword bpc (→0), zerospace 50. sword bpo left at 1.
  • ongoing has no original_type; all 5 produced in one tick at height 110.

Pending JSON (services.cpp:903-915)#

{
  "type": "construct",
  "blueprint": "sword bpo",
  "output": { "sword": 2 }
}

(ConstructionTests.PendingJson, services_tests.cpp:1465-1477.)

Real-config construction example#

rv s (Looter, red) (items/vehicles_others.pb.text:1-12): complexity: 10, with_blueprint: true, resources {mat a: 52500, mat b: 22500}. On mainnet (construction_cost: 1, construction_blocks: 1), building one rv s from a bpc:

  • base cost = 1 × (1 × 10) = 10 vCHI burnt.
  • consumes 52,500 Agarite (mat a) + 22,500 Borolium (mat b) + 1 rv s bpc.
  • duration = 1 × 10 = 10 blocks.
  • requires a building offering vehicle_construction.

Fitment example lf beam (Light Tachyon Beam, items/fitments.pb.text:1-26): complexity: 2, resources {mat a: 2500, mat b: 2500}. Mainnet base cost 1×(1×2) = 2 vCHI, duration 2 blocks, requires item_construction.


9. Ongoing-operation lifecycle (blocks & completion)#

Multi-block operations (armour repair, blueprint copy, construction, plus building construction and building config update) are stored as OngoingOperation rows (proto/ongoing.proto:142-168). Each row carries:

  • start_height (set at creation, services.cpp:1051-1055),
  • the target processing height (DB column, set via op->SetHeight(...)),
  • an associated character ID and/or building ID (DB columns),
  • a oneof op body identifying the operation type.

How they complete#

ProcessAllOngoings (ongoings.cpp:147-230) runs once per block:

  1. Query all ongoings whose target height == current height (QueryForHeight, with a CHECK_EQ that none are overdue, ongoings.cpp:158-166).
  2. Dispatch on op_case() (ongoings.cpp:176-223):
    • kArmourRepair → set armour to max, clear character ongoing.
    • kBlueprintCopyUpdateBlueprintCopy (give 1 copy; reschedule or return original).
    • kItemConstructionUpdateItemConstruction (give items; reschedule or return original).
    • (also kProspection, kBuildingConstruction, kBuildingUpdate — not crafting services but processed here too.)
  3. Delete all processed ongoings for this height (ongoings.cpp:229). Note that for serial copy/construction, the row's height was already pushed forward during the update so it is not deleted yet.

"Busy" semantics: only characters are blocked by ongoings (armour repair, prospecting). The c->IsBusy() invariant is re-checked post-processing (ongoings.cpp:225-226). Blueprint copy and construction are tied to a building, not a character, so they don't make any character busy and a player can queue several in the same building.

Building destruction while crafting (combat.cpp:1164-1277)#

If a building is destroyed (combat) mid-operation, KillProcessor::ProcessBuilding salvages a limited amount into the "combined inventory" that is then partially dropped on the ground:

  • Blueprint copy in progress: only the original bpo is recovered (combat.cpp:1208-1214). Any copies not yet finished, plus the in-flight job, are lost.
  • Construction in progress: only the original blueprint is recovered, and only if the job was from an original (combat.cpp:1216-1222). Construction resources already consumed and any not-yet-produced items are lost. Jobs from bpc recover nothing (the bpcs were already consumed).
  • All recovered items + remaining building/account/character inventories go into a combined pile, of which each stack is dropped to the ground with probability BUILDING_INVENTORY_DROP_PERCENT = 30% (combat.cpp:47, 1255-1271) — i.e. ~70% of contents are destroyed outright.
  • All ongoings for the building are then deleted (combat.cpp:1274).

This is a significant risk factor for queuing long crafting jobs in a contestable (player-owned, non-ancient) building.


10. Constant & parameter reference#

All from proto/roconfig/params.pb.text (mainnet/testnet) and proto/roconfig/test_params.pb.text (regtest override), unless noted.

Parameter Mainnet/testnet Regtest Used by
armour_repair_cost_millis 100 (1 vCHI / 10 HP) 100 repair base cost (services.cpp:381)
armour_repair_hp_per_block 100 100 repair duration (services.cpp:409)
bp_copy_cost (vCHI / complexity / copy) 1 100 copy base cost (services.cpp:633)
bp_copy_blocks 1 10 declared but unused by copy timing
construction_cost (vCHI / complexity / item) 1 100 construct base cost (services.cpp:793)
construction_blocks (blocks / complexity) 1 10 copy and construct duration (services.cpp:749, 959)
building_update_delay 120 10 delay for service-fee changes (ongoings.cpp:206)
RevEng base chance 1/N 1/10 (MAIN/TEST/POLYGON/MUMBAI) 1/1 (REGTEST/GANACHE) reveng odds (params.cpp:60-72)
RevEng diminishing factor ×4/3 per existing copy same params.cpp:82-88
RevEng floor 1 / 1,000,000,000 same params.cpp:55, 87
MAX_SERVICE_FEE_PERCENT 1000 (code constant) 1000 fee cap (moveprocessor.cpp:48)
MAX_QUANTITY 2^50 (code constant) 2^50 n upper bound (database/inventory.hpp:54)
BUILDING_INVENTORY_DROP_PERCENT 30 (code constant) 30 salvage drop on destroy (combat.cpp:47)

The chain enum drives only the RevEng base chance (10 on real networks incl. Polygon, 1 on regtest/ganache). All other crafting numbers differ between mainnet/testnet and regtest only through the test_params.pb.text overrides above. Mainnet and testnet share the same crafting params.


11. Display-name cross-reference#

Internal codes used above with their UI names (the names shown in the official client):

Code Display name
raw araw i Trimideum, Talon, Henoix, Orchanum, Kalanite, Voltar, Ravolute, Talgarite, Liberite
mat amat i Agarite, Borolium, Chronogen, DARR-4, Exillium, FORTON-D, Greophite, Helion, I-77E
art c / art uc / art r / art ur Ancient Artefact (common / uncommon / rare / ultra-rare)
vhf refinery Mobile Refinery
rv s Looter (red small vehicle)
lf beam Light Tachyon Beam
<x> bpo / <x> bpc " Blueprint (Original)" / "(Copy)" — generated; the game UI may not list every one

Buildings have no display-name mapping in the game UI; clients show the raw type codes (ancient1, r_cc, b_vb, etc.). Item codes such as bow, sword, test ore, foo, bar, zerospace are test-only fixtures from items/test.pb.text and do not exist in the live game.


Open questions#

  1. bp_copy_blocks appears unused. The config defines bp_copy_blocks (mainnet 1, regtest 10) and the proto comments it as "Number of blocks for copying a blueprint, per complexity" (proto/config.proto:566-567). However GetBpCopyBlocks computes copy duration from construction_blocks × complexity, not bp_copy_blocks (services.cpp:749). On regtest both are 10 so the discrepancy is invisible in tests, but on mainnet copy duration uses construction_blocks (1) not bp_copy_blocks (1) — identical there too. The field looks vestigial; confirm whether any other code path reads it or if it is dead config.

  2. Per-building-type service-pair mapping for *_c1*_c11. This doc lists the general pattern (armour_repair + one production service) and exact mappings for the headline types; designers building a full UI should read each <faction>_c<N>.pb.text directly, as the production service paired with repair varies per index and is not summarised in a single config table.

  3. Mobile-refinery cargo-overflow corner case. The code comments assert refining always frees cargo so a mobile refinery can't overflow (services.cpp:256-258). This relies on every refined output being strictly smaller in total space than its input. It holds for the shipped material configs, but it is an implicit invariant of the item config rather than an enforced runtime check — worth flagging if new ores/materials are added.