Shipping layouts

The two ways to hand BusinessBridge your BusinessLayoutSet JSON — in memory from your AssetBundle, or as files on disk — and the asset-naming rule that trips people up.

A layout is one BusinessLayoutSet in the game's own JSON format: the fixtures, shelves and products an AI store of your business type is built from. You supply one per building size/version you want the AI to be able to use. Which sizes those are is your decision; BusinessBridge imposes no list and never substitutes one size for another.

Two transports, one validator

FieldUse it when
LayoutContents Your layouts ship inside your AssetBundle, which is the normal case. Contract stage beta-2 and later.
LayoutFiles Your layouts really are loose .json files on disk — a development mod, or content you write at runtime. Supported since beta-1 and still supported.

Set either, or both: they become one list as soon as BusinessBridge has them, and from that point on the same deserializer, the same rules and the same messages apply. A layout cannot pass validation because of how it arrived. Two layouts claiming the same BuildingSize and BuildingVersion are rejected whichever list they came from.

The shape of LayoutContents

public List<AiBusinessLayout> LayoutContents = new List<AiBusinessLayout>();

public sealed class AiBusinessLayout
{
    public string Name;
    public string Json;
}

Json is the text of the layout file. Name is the logical layout name — it stands in for what the file name would have been, so it must equal the registration's LayoutName. It becomes the layout's name, overriding whatever LayoutName the JSON itself carries, and it labels the layout in validation messages.

The field is a string rather than a TextAsset on purpose: BusinessBridge never holds a reference to an asset whose bundle you may unload, and you stay free to load the text however you like.

Give every layout asset a unique file name

One LayoutName covers all of your building sizes. So the obvious layout on disk — YourShopImporters.json in a folder per size — puts several identically named assets into one AssetBundle. Unity generates deterministic ids from asset names, and duplicates fail the build with a hash collision that can still leave a valid-looking but empty bundle behind.

Put the size — and, if one bundle carries several business types, the type — in the file name, and pass the logical layout name explicitly instead of asset.name.

Bundling and loading several sizes

A .json file in your mod folder is imported as a TextAsset, so it travels in your bundle with everything else. Name the files so they are unique:

Assets/Mods/YourMod/Layouts/C1/yourshop_C1_layout.json
Assets/Mods/YourMod/Layouts/C2/yourshop_C2_layout.json
Assets/Mods/YourMod/Layouts/D2/yourshop_D2_layout.json

Then load them and assign the logical name yourself:

const string LayoutName = "YourShopImporters";   // the logical AI layout name
var sizes = new[] { "C1", "C2", "D2" };
var layouts = new List<AiBusinessLayout>();

var bundle = AssetService.GetBundle(context.ModId, YourBundleKey);
foreach (var size in sizes)
{
    var asset = bundle.LoadAsset<TextAsset>($"Assets/Mods/YourMod/Layouts/{size}/yourshop_{size}_layout.json");
    if (asset == null || string.IsNullOrEmpty(asset.text)) continue;   // log it; do not guess a substitute
    layouts.Add(new AiBusinessLayout(LayoutName, asset.text));         // LayoutName, NOT asset.name
}

Two different names

The asset file name only has to locate the asset in your bundle, and must be unique within it. The logical layout name (AiBusinessLayout.Name and the registration's LayoutName) is what the game matches AiBusinessDefault.buildingLayout against, and is shared by all your sizes. They are separate concepts and do not have to match.

What tells your sizes apart is BuildingSize and BuildingVersion inside each JSON document — never the file name.

If you use LayoutFiles instead

There BusinessBridge takes the layout's name from the path, exactly as the game does, so each file must be named <LayoutName>.json — one per size, in separate folders. This is the beta-1 behaviour and it has not changed.

LayoutFiles = { c1Path, c2Path },   // each file named YourShopImporters.json

What is checked

Per layout, whichever transport it arrived on:

  • the text parses as a BusinessLayoutSet
  • its BusinessType is the business type you are registering
  • its layout name equals the registration's LayoutName
  • BuildingSize is non-empty, BuildingVersion is 1 or more, and no two layouts claim the same size and version
  • every item, and every product a shelf sells, is a real item in this build
  • any primary product of your business type is a demanded product
  • at least one shelf actually sells something

Any of those failing rejects the whole registration: nothing is stored and nothing is injected. A layout for a building size the city has no building of is reported as a note instead — it is valid, the AI simply never gets to use it. See Registering a business type for the full list and the messages.

{# GoatCounter's own count.js, served from this site rather than from a third-party host. The context processor decides whether this page may be counted at all; see core/context_processors.py. Both scripts are deferred, so the settings file below runs first and has set window.goatcounter.referrer before count.js reads it. #}