t1k:unity:tof:blueprint-csv
| Field | Value |
|---|---|
| Module | tof |
| Version | 2.2.2 |
| Effort | low |
| Tools | — |
Keywords: battlepass, blueprint, csv, csvcolumn, csvdata, csvrow, donate, generic blueprint reader, idatamanager, progression reward, theone.data, theonefeature, tof
How to invoke
Section titled “How to invoke”/t1k:unity:tof:blueprint-csvTheOneFeature Blueprint CSV Editing
Section titled “TheOneFeature Blueprint CSV Editing”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 olderGameFoundation.BlueprintFlowGenericBlueprintReaderByRow<,>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)”The class pair
Section titled “The class pair”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) orCsvData<T>(list). This replacesGenericBlueprintReaderByRow<,>/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
ICsvDatabecomes a nested sub-table (advanced; seeCsvSerializerfor the row-grouping rules). - Parser: CsvHelper (
org.nuget.csvhelper) +IConverterManager. Requires theTHEONE_CSVandTHEONE_UNITASKscripting defines.
Do not use
[CsvHeaderKey]on aCsvData<,>record — that attribute belongs to the old BlueprintFlow reader and is ignored byCsvSerializer. If you see it on aCsvData<,>record (e.g.PeriodRewardAdsBlueprint) it is a migration leftover that happens to be harmless only because the marked column is already first.
Loading
Section titled “Loading”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.
File location
Section titled “File location”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.
CSV format rules (both mechanisms)
Section titled “CSV format rules (both mechanisms)”- First row is the header. Column names bind to the record’s properties/fields (case-sensitive).
- Empty cells: leave blank — do not write
nullor"". 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:
- CSV asset anywhere under
Assets/(e.g.Assets/_Game/<Game>/Blueprints/<Name>Blueprint.csv). - Addressable entry — add the CSV to the project’s
Blueprintsaddressable group (Assets/AddressableAssetsData/AssetGroups/Blueprints.asset) withm_Address= the blueprint class name (e.g.FooBlueprint). The address MUST equaltype.GetKey()— which is the[TheOne.Extensions.Key("…")]value, or the type name when no[Key]is present.AssetTextDataStorageloads theTextAssetat that address viaIAssetsManager. (Verified: existing entriesPeriodRewardAdsBlueprint,ProgressionRewardBlueprintuse address = class name.) Register via the editor Addressables window orAddressableAssetSettings.CreateOrMoveEntry(guid, group)+entry.address = className. - Loader — a service that injects
IDataManagerand callsLoadAsync<FooBlueprint>()so the data is cached before gameplay reads it. Model it on the consumer pattern (WallpaperCollectionService) or a dedicatedIAsyncEarlyLoadable:
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:
- 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. - 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.
Logic/Visual family convention (recommended for option 2)
Section titled “Logic/Visual family convention (recommended for option 2)”When you split, use the symmetric suffix so both files are self-describing:
| Family | Class / file | Holds |
|---|---|---|
| Logic | <Feature>LogicBlueprint | Id + numbers, gates, enum categories, cooldowns, rates, and mechanical keys (Ability1="FeralPounce", EffectKey) |
| Visual | <Feature>VisualBlueprint | Id + 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 thoughDisplayNameis one short string, it is presentation, not logic.- Spawnables (heroes, enemies, pets, projectiles, VFX) →
PrefabAddressin Visual. Items →IconAddress+ 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>Keyin the Logic blueprint + the human-readable<Thing>Description/<Thing>Textin the Visual blueprint, joined onId. (e.g. HeroPassiveKey=PrimalResiliencein Logic ↔PassiveName+PassiveDescriptionin 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 shippedRealms.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!).
Verification
Section titled “Verification”- Enter Play mode and trigger the feature that consumes the blueprint; confirm the records resolve (e.g.
blueprint[id]returns data, noKeyNotFound/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 foundunless 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 + isLoadFromResourcepublic class CardCollectionCardsBlueprint : GenericBlueprintReaderByRow<string, CardRecord> { }
[CsvHeaderKey("Id")] // marks the key columnpublic 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 noGameLifetimeScopeentry. - Loaded by
BlueprintReaderManager.LoadBlueprint()(invoked fromDataLoader). In resource mode (IsResourceModeor[BlueprintReader(..., isLoadFromResource: true)]) it reads a plaintextTextAssetfromResources/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>().
Gotchas
Section titled “Gotchas”- No encryption /
EncryptedData/folder inTheOne.Dataprojects. 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
GameFoundationreader, edit the submodule repo and PR there — not the consumer project. [CsvHeaderKey]is old-API. It does nothing on aCsvData<,>record.