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:
- The operation is not inside a building (mobile refinery has no fee) —
building == nullptr(services.cpp:993-997). - 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). - 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_percentis 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 3test 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_percentis itself delayed (anti-front-running): the new config takes effect afterbuilding_update_delayblocks (mainnet 120, regtest 10) via abuilding_updateongoing 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 foundation —
b->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 inIsFullyValid→IsSupported(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.textfor the exact pair. Ther_/g_/b_prefix is the faction (red/green/blue) and only matters for who may construct the building, not for service eligibility. Obelisks and*_rtbuildings 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 arefinesblock (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, +2bar, +1zerospace(ServicesTests.BasicOperation,services_tests.cpp:129-143). - Refine
n:9→ 3 steps → base 30, +6bar, +3zerospace(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 a…raw 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_unitsonly: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.AlreadyRepairingconfirms you can't stack repairs (services_tests.cpp:834-854). - There must be missing armour HP (
maxArmour - curArmour > 0,services.cpp:364-371).RepairTests.NoMissingHprejects 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_millisis 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 arevengblock (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)#
- Remove all
ninput artefacts up front (services.cpp:554-555). - 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. - For each of the
ntrials 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 / NwhereN = 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).
- Uniformly pick one candidate output type from the eligible list
(
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 (abpc), and unknown items (services.cpp:649-674;BlueprintCopyTests.InvalidItemTyperejectsswordandsword 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_blocksis defined in config but the duration formula actually usesconstruction_blocks, notbp_copy_blocks— see below.bp_copy_blocksappears unused by the copy timing code.
Duration / completion (per-copy serial production)#
Execution (services.cpp:713-735):
- Remove 1 original from the inventory immediately
(
inv.AddFungibleCount(original, -1),services.cpp:721). The original is "locked" for the whole job. - Create an ongoing
blueprint_copywithnum_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
bpcto the account's building inventory. - If copies remain (
num_copies − 1 > 0): decrement and reschedule anotherbaseDurationblocks ahead (copies are produced one at a time, serially). - When the last copy completes: also return the original
bpoto 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 bpoorsword bpcis in inventory yet (both 0 at start of the job). - Ongoing
blueprint_copycreated withaccount=domob,original_type="sword bpo",copy_type="sword bpc",num_copies=10, first tick at height100 + 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'sfor_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 buildred fitmentfromred fitment bpo, a RED account can (services_tests.cpp:1334-1361). - For every entry in the output's
construction_resourcesmap, the building inventory must holdn × 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)#
- Subtract all
n × resourceamounts from the building inventory (services.cpp:924-929). - Subtract blueprints: 1 if from original, else
n(services.cpp:931-934). - Create an ongoing
item_constructionscheduled after the time for one item:op.height = currentHeight + GetConstructionBlocks(output)whereGetConstructionBlocks = construction_blocks × complexity(services.cpp:936-951,services.cpp:955-960). The ongoing storesaccount,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): allnitems 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, decrementsnum_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 × complexityblocks (one batch). - from original:
n × construction_blocks × complexityblocks (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),zerospace50 (100 → 50).sword bpcuntouched (1). - ongoing
item_construction: account=domob, output=sword, num_items=5, original_type=sword bpo, first tick at100 + 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),zerospace50.sword bpoleft 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) + 1rv s bpc. - duration =
1 × 10 = 10blocks. - 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 opbody identifying the operation type.
How they complete#
ProcessAllOngoings (ongoings.cpp:147-230) runs once
per block:
- Query all ongoings whose target height == current height
(
QueryForHeight, with aCHECK_EQthat none are overdue,ongoings.cpp:158-166). - Dispatch on
op_case()(ongoings.cpp:176-223):kArmourRepair→ set armour to max, clear characterongoing.kBlueprintCopy→UpdateBlueprintCopy(give 1 copy; reschedule or return original).kItemConstruction→UpdateItemConstruction(give items; reschedule or return original).- (also
kProspection,kBuildingConstruction,kBuildingUpdate— not crafting services but processed here too.)
- 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
bpois 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 a … raw i |
Trimideum, Talon, Henoix, Orchanum, Kalanite, Voltar, Ravolute, Talgarite, Liberite |
mat a … mat 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 |
" |
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#
bp_copy_blocksappears unused. The config definesbp_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). HoweverGetBpCopyBlockscomputes copy duration fromconstruction_blocks × complexity, notbp_copy_blocks(services.cpp:749). On regtest both are 10 so the discrepancy is invisible in tests, but on mainnet copy duration usesconstruction_blocks (1)notbp_copy_blocks (1)— identical there too. The field looks vestigial; confirm whether any other code path reads it or if it is dead config.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.textdirectly, as the production service paired with repair varies per index and is not summarised in a single config table.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 totalspacethan 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.