Skip to content

t1k:unity:tof:blueprint-csv

FieldValue
Moduletof
Version2.2.2
Effortlow
Tools

Keywords: battlepass, blueprint, csv, csvcolumn, csvdata, csvrow, donate, generic blueprint reader, idatamanager, progression reward, theone.data, theonefeature, tof

/t1k:unity:tof:blueprint-csv

Blueprints are CSV-driven configs read at runtime. A blueprint is two parts: a plaintext .csv (the data) and a C# class pair (a container + a row record) that the CSV columns bind to.

Two mechanisms exist. Author NEW blueprints with TheOne.Data.CsvData<,> (current). The older GameFoundation.BlueprintFlow GenericBlueprintReaderByRow<,> is deprecated (see § Deprecated). Many existing features still use the old reader; migrate opportunistically, do not rewrite en masse.

✅ Current mechanism — TheOne.Data (com.theone.data)

Section titled “✅ Current mechanism — TheOne.Data (com.theone.data)”
namespace MyGame.Features.Foo.Models
{
using TheOne.Data;
// Container: a dictionary keyed by the first column (or the [CsvRow] key).
public sealed class FooBlueprint : CsvData<string, FooRecord> { }
// Row record: a plain POCO. Each column binds to a property/field by name.
public sealed class FooRecord
{
public string Id { get; set; } = null!; // first column → dictionary key (default)
public int Cost { get; set; }
public Rarity Rarity { get; set; } // enums/Vector/Color auto-convert
}
}
  • Container base: CsvData<TKey, TValue> (dictionary) or CsvData<T> (list). This replaces GenericBlueprintReaderByRow<,> / GenericBlueprintReaderByCol.
  • Key column: by default the first column is the dictionary key. Override with [CsvRow(prefix, key)] on the record (key = the column name to use as key; prefix = a string prepended to every column name).
  • Column name: defaults to the property/field name. Override per field with [CsvColumn("Header Name")].
  • [CsvIgnore] — skip a field. [CsvOptional] — field’s column may be absent from the CSV (no throw).
  • Nested tables: a field whose type is itself an ICsvData becomes a nested sub-table (advanced; see CsvSerializer for the row-grouping rules).
  • Parser: CsvHelper (org.nuget.csvhelper) + IConverterManager. Requires the THEONE_CSV and THEONE_UNITASK scripting defines.

Do not use [CsvHeaderKey] on a CsvData<,> record — that attribute belongs to the old BlueprintFlow reader and is ignored by CsvSerializer. If you see it on a CsvData<,> record (e.g. PeriodRewardAdsBlueprint) it is a migration leftover that happens to be harmless only because the marked column is already first.

Loaded through TheOne.Data.IDataManager, keyed by the type:

var foo = await this.dataManager.LoadAsync<FooBlueprint>(); // or dataManager.Load<FooBlueprint>()
var record = foo["someId"];

The storage key is type.GetKey() — the value of [TheOne.Extensions.Key("…")] on the class, or the type name if absent. The DataManager’s storage registration maps that key to the physical CSV; see the DataManager* DI installer for the storage/path binding.

Author the plaintext CSV beside its feature, e.g. Assets/<Feature>/Blueprints/<Name>BlueprintTemplate.csv (TOF ships …Template.csv defaults; consumers override). Project-local game blueprints can live under the game folder or Assets/Resources/BlueprintData/<path>/<Name>.csv when loaded through the Asset/Resources storage. There is no encryption step in current TheOne.Data projects — the runtime reads the plaintext CSV directly.

  • First row is the header. Column names bind to the record’s properties/fields (case-sensitive).
  • Empty cells: leave blank — do not write null or "". Blank → the converter’s default for that type.
  • A comma inside a value: wrap the whole cell in double quotes.
  • Line endings: match the existing file (LF vs CRLF) to avoid noisy diffs.
  • No trailing blank line — the parser can read it as an empty-key row and throw.

Loading & runtime wiring (verified end-to-end)

Section titled “Loading & runtime wiring (verified end-to-end)”

A CsvData<,> class does not load by sitting in a folder — it resolves through IDataManager → an asset address. Wiring a new game blueprint (not a TOF default) takes three steps:

  1. CSV asset anywhere under Assets/ (e.g. Assets/_Game/<Game>/Blueprints/<Name>Blueprint.csv).
  2. Addressable entry — add the CSV to the project’s Blueprints addressable group (Assets/AddressableAssetsData/AssetGroups/Blueprints.asset) with m_Address = the blueprint class name (e.g. FooBlueprint). The address MUST equal type.GetKey() — which is the [TheOne.Extensions.Key("…")] value, or the type name when no [Key] is present. AssetTextDataStorage loads the TextAsset at that address via IAssetsManager. (Verified: existing entries PeriodRewardAdsBlueprint, ProgressionRewardBlueprint use address = class name.) Register via the editor Addressables window or AddressableAssetSettings.CreateOrMoveEntry(guid, group) + entry.address = className.
  3. Loader — a service that injects IDataManager and calls LoadAsync<FooBlueprint>() so the data is cached before gameplay reads it. Model it on the consumer pattern (WallpaperCollectionService) or a dedicated IAsyncEarlyLoadable:
public sealed class FooBlueprintService : TheOne.Lifecycle.IAsyncEarlyLoadable
{
private readonly IDataManager dataManager;
[Preserve] public FooBlueprintService(IDataManager dataManager) => this.dataManager = dataManager;
public FooBlueprint Foos { get; private set; } = null!;
async UniTask IAsyncEarlyLoadable.LoadAsync(IProgress<float>? p, CancellationToken ct)
=> this.Foos = await this.dataManager.LoadAsync<FooBlueprint>(progress: p, cancellationToken: ct);
}

Register it in the composition root with builder.Register<FooBlueprintService>(Lifetime.Singleton).AsImplementedInterfaces().AsSelf();AsImplementedInterfaces is what makes the GameFoundation lifecycle run IAsyncEarlyLoadable.LoadAsync during boot (same pattern as LevelConfigService). No per-blueprint DI line is needed for the CsvData<,> type itself.

Data modeling — separate logic data from visualization data

Section titled “Data modeling — separate logic data from visualization data”

Keep logic data (gameplay numbers, ids, formulas, rarity, cooldowns, costs, unlock gates) separate from visualization data (which prefab / sprite / icon / VFX / sound / animation an entity uses). Two ways, in order of preference:

  1. Separate columns, clearly grouped — a single CSV may carry both, but put all the asset-reference columns together and name them unambiguously as addressable keys (IconAddress, PrefabAddress, SfxAddress), never raw file paths.
  2. Separate CSV/blueprint keyed by the same Id — the preferred form for any entity with real presentation data. Prefer this when the art bindings are large, owned by a different discipline, or swapped per skin/era.

Why: designers tune balance without touching art bindings (and vice-versa); art can be re-skinned without a balance review; logic stays unit-testable without pulling in Addressables/asset refs. Asset references are always addressable keys/AssetReference, not file paths — paths rot on move/rename. If a CSV mixes a damage number and a .prefab path in the same conceptual column, that’s the smell this rule catches.

Section titled “Logic/Visual family convention (recommended for option 2)”

When you split, use the symmetric suffix so both files are self-describing:

FamilyClass / fileHolds
Logic<Feature>LogicBlueprintId + numbers, gates, enum categories, cooldowns, rates, and mechanical keys (Ability1="FeralPounce", EffectKey)
Visual<Feature>VisualBlueprintId + DisplayName, all *Name, description prose, and asset keys: PrefabAddress / IconAddress / VfxAddress / SfxAddress / BackdropAddress / MusicAddress

Boundary rules that resolve the common ambiguities:

  • DisplayName, any name, and any music/backdrop/sprite/prefab/icon binding are VISUAL — even though DisplayName is one short string, it is presentation, not logic.
  • Spawnables (heroes, enemies, pets, projectiles, VFX) → PrefabAddress in Visual. ItemsIconAddress + name in Visual.
  • Mechanical KEYS stay in Logic (an ability/effect identifier the gameplay code resolves is logic, not display text).
  • Prose that encodes a mechanic (a passive effect sentence, a skill effect description) is BOTH player-facing text AND the mechanic → DUPLICATE it: a structured <Thing>Key in the Logic blueprint + the human-readable <Thing>Description/<Thing>Text in the Visual blueprint, joined on Id. (e.g. Hero PassiveKey=PrimalResilience in Logic ↔ PassiveName+PassiveDescription in Visual.)
  • Unbuilt art → use the sentinel - as the address value so the column exists and validates now, resolving to nothing until the asset lands. (Mirrors the shipped Realms.csv - convention.)

Folder organization: put the data CSVs under Blueprints/Logic/ + Blueprints/Visual/ and the record classes under Scripts/Runtime/Blueprints/Logic/ + …/Visual/. The addressable address must equal the class name regardless of folder, so blueprints can be freely re-foldered without touching the loader (IDataManager.LoadAsync<T>() resolves by address = class name, not path).

SSOT precedence: if a shipped balance CSV (e.g. StreamingAssets/Balance/*.csv) and a wiki/design doc disagree, the shipped CSV wins — extract logic blueprints from the shipped data, not the roadmap prose, unless told otherwise.

Int keys are fine: CsvData<int, TRecord> works when the first column is an integer id (e.g. RealmId 0/1/2); the record property is public int RealmId { get; } (no = null!).

  • Enter Play mode and trigger the feature that consumes the blueprint; confirm the records resolve (e.g. blueprint[id] returns data, no KeyNotFound/load warning in the console).
  • Schema change (added/removed/renamed column) → update the record class in the same change. A CSV-only edit silently drops the unmapped column; a class-only edit throws Column … not found unless the field is [CsvOptional]/[CsvIgnore].

⚠️ Deprecated mechanism — GameFoundation.BlueprintFlow

Section titled “⚠️ Deprecated mechanism — GameFoundation.BlueprintFlow”

The old reader lives in the GameFoundation.BlueprintFlow assembly (Packages/com.gdk.core, submodule The1Studio/GameFoundation). It is being marked [Obsolete] in favour of TheOne.Data.

using BlueprintFlow.BlueprintReader;
[BlueprintReader("TheOneFeature/CardCollection/Cards", true)] // path + isLoadFromResource
public class CardCollectionCardsBlueprint : GenericBlueprintReaderByRow<string, CardRecord> { }
[CsvHeaderKey("Id")] // marks the key column
public class CardRecord { public string Id { get; set; } /* … */ }

Old-way facts (for reading/maintaining legacy features, not for new work):

  • Reader classes are auto-registered in DI by reflection (typeof(IGenericBlueprintReader).GetDerivedTypes()), so they need no GameLifetimeScope entry.
  • Loaded by BlueprintReaderManager.LoadBlueprint() (invoked from DataLoader). In resource mode (IsResourceMode or [BlueprintReader(..., isLoadFromResource: true)]) it reads a plaintext TextAsset from Resources/BlueprintData/<DataPath>.csv; otherwise from a downloaded LiveOps bundle, falling back to Resources. Parser: Sylvan.Data.Csv.
  • TOF defaults live at Assets/Resources/BlueprintData/TheOneFeature/<Name>.csv.

Migration sketch: swap the base type GenericBlueprintReaderByRow<K,V>CsvData<K,V> (namespace TheOne.Data); drop [BlueprintReader]; replace [CsvHeaderKey("X")] with [CsvRow(prefix:"", key:"X")] only if X is not already the first column; change the consumer’s load path from injected reader to IDataManager.Load<T>().

  • No encryption / EncryptedData/ folder in TheOne.Data projects. Older docs describing a “plaintext + encrypted twin” pair and an “Encrypt Blueprints” tool do not apply here — verify the project actually has that pipeline before assuming it.
  • Columns bind by name, not order — renaming a property without updating the CSV header drops that column silently (or throws if the field is required).
  • Submodule files are READ-ONLY from the consumer side. To change a TOF default blueprint or the GameFoundation reader, edit the submodule repo and PR there — not the consumer project.
  • [CsvHeaderKey] is old-API. It does nothing on a CsvData<,> record.