Add Alina’s Map Mod 1.7.25304.436

https://rmh.alinanova.dev/mod/cba5981c-8e8e-4b01-918c-214fa155adff
This commit is contained in:
2025-10-31 22:33:42 -05:00
parent d6152d88f0
commit 48a08f4b74
8 changed files with 1809 additions and 0 deletions

Binary file not shown.

View File

@@ -0,0 +1,636 @@
{
"items": {
"AN_Sylva_Interchange_Yard": {
"identifier": "AN_Sylva_Interchange_Yard",
"name": "Sylva Interchange Yard",
"groupIds": [
"AN_Sylva_Interchange_Yard"
],
"description": "A yard that can be useful for organizing west bound trains and storing cars if the Interchange is filled to capacity.",
"deliveryPhases": [
{
"cost": 2000,
"deliveries": [
{
"carTypeFilter": "GB*",
"count": 4,
"load": "ballast",
"direction": 0
},
{
"carTypeFilter": "GB*",
"count": 12,
"load": "gravel",
"direction": 0
}
]
},
{
"cost": 2000,
"deliveries": [
{
"carTypeFilter": "GB*",
"count": 2,
"load": "gravel",
"direction": 0
},
{
"carTypeFilter": "GB*",
"count": 8,
"load": "ties",
"direction": 0
},
{
"carTypeFilter": "FM*",
"count": 6,
"load": "rails",
"direction": 0
}
]
}
],
"prerequisiteSections": [
"s1"
],
"area": "sylva",
"trackSpans": [
"PAN_Sylva_Interchange_Yard_00"
],
"industryComponent": "AN_Sylva_Interchange_Yard.interchange-yard-site"
},
"AN_Sylva_Wye": {
"identifier": "AN_Sylva_Wye",
"name": "Sylva Wye",
"groupIds": [
"AN_Sylva_Wye"
],
"description": "Adds a Wye at the Sylva Interchange, great for turning around those massive Berks.",
"area": "sylva",
"trackSpans": [
"PAN_Sylva_Wye_00"
],
"deliveryPhases": [
{
"cost": 2000,
"deliveries": [
{
"carTypeFilter": "FM",
"count": 1,
"load": "mow-machinery",
"direction": 0
},
{
"carTypeFilter": "GB",
"count": 100,
"load": "debris",
"direction": 0
}
]
},
{
"cost": 2000,
"deliveries": [
{
"carTypeFilter": "FM",
"count": 1,
"load": "mow-machinery",
"direction": 1
},
{
"carTypeFilter": "GB*",
"count": 6,
"load": "ballast",
"direction": 0
},
{
"carTypeFilter": "GB*",
"count": 5,
"load": "gravel",
"direction": 0
},
{
"carTypeFilter": "GB*",
"count": 2,
"load": "ties",
"direction": 0
},
{
"carTypeFilter": "FM*",
"count": 2,
"load": "rails",
"direction": 0
}
]
}
],
"prerequisiteSections": [
"s1",
"AN_Sylva_Interchange_Yard"
],
"industryComponent": "AN_Sylva_Wye.wye-site"
},
"AN_Sylva_Paper_Crossover": {
"identifier": "AN_Sylva_Paper_Crossover",
"name": "Sylva Paper Crossovers",
"groupIds": [
"AN_Sylva_Paper_Crossover"
],
"description": "Adds two Crossovers at Sylva Paperboard.",
"area": "sylva",
"trackSpans": [
"PAN_Sylva_Paper_Crossover_00"
],
"deliveryPhases": [
{
"cost": 1000,
"deliveries": [
{
"carTypeFilter": "GB*",
"count": 1,
"load": "ballast",
"direction": 0
},
{
"carTypeFilter": "GB*",
"count": 1,
"load": "ties",
"direction": 0
},
{
"carTypeFilter": "FM*",
"count": 1,
"load": "rails",
"direction": 0
}
]
}
],
"prerequisiteSections": [
"s1"
],
"industryComponent": "AN_Sylva_Paper_Crossover.sylva-paper-crossover-site"
},
"AN_Sylva_Pax_Storage": {
"identifier": "AN_Sylva_Pax_Storage",
"name": "Sylva Pax Storage",
"groupIds": [
"AN_Sylva_Pax_Storage"
],
"description": "Adds two storage tracks to Sylva Station.",
"area": "sylva",
"trackSpans": [
"PAN_Sylva_Pax_Storage_00"
],
"deliveryPhases": [
{
"cost": 2000,
"deliveries": [
{
"carTypeFilter": "GB*",
"count": 2,
"load": "ballast",
"direction": 0
},
{
"carTypeFilter": "GB*",
"count": 2,
"load": "ties",
"direction": 0
},
{
"carTypeFilter": "FM*",
"count": 2,
"load": "rails",
"direction": 0
}
]
}
],
"prerequisiteSections": [
"s1"
],
"industryComponent": "AN_Sylva_Pax_Storage.sylva-station-site"
},
"AN_Whittier_Yard_Sawmill": {
"identifier": "AN_Whittier_Yard_Sawmill",
"name": "Whittier Sawmill Connection",
"groupIds": [
"AN_Whittier_Yard_Sawmill"
],
"description": "Extend the sawmill track over to the interchange",
"trackSpans": [
"PAN_Whittier_Yard_00"
],
"area": "whittier",
"industryComponent": "AN_Whittier_Yard.sawmill-site",
"deliveryPhases": [
{
"cost": 2000,
"deliveries": [
{
"carTypeFilter": "GB*",
"count": 4,
"load": "ballast",
"direction": 0
},
{
"carTypeFilter": "GB*",
"count": 2,
"load": "gravel",
"direction": 0
},
{
"carTypeFilter": "GB*",
"count": 2,
"load": "ties",
"direction": 0
},
{
"carTypeFilter": "FM*",
"count": 1,
"load": "rails",
"direction": 0
}
]
}
]
},
"AN_Whittier_Yard_1": {
"identifier": "AN_Whittier_Yard_1",
"name": "Whittier Yard ",
"groupIds": [
"AN_Whittier_Yard_Yard_Lead",
"AN_Whittier_Yard_Yard_Track_6",
"AN_Whittier_Yard_Yard_Track_7",
"AN_Whittier_Yard_Yard_Track_8"
],
"trackSpans": [
"PAN_Whittier_Yard_00"
],
"industryComponent": "AN_Whittier_Yard.yard-site-1",
"area": "whittier",
"description": "A yard that can be useful for organizing trains and storing cars.",
"prerequisiteSections": [
"AN_Whittier_Yard_Sawmill"
],
"deliveryPhases": [
{
"cost": 2000,
"deliveries": [
{
"carTypeFilter": "GB*",
"count": 8,
"load": "ballast",
"direction": 0
},
{
"carTypeFilter": "GB*",
"count": 12,
"load": "gravel",
"direction": 0
}
]
},
{
"cost": 2000,
"deliveries": [
{
"carTypeFilter": "GB*",
"count": 6,
"load": "gravel",
"direction": 0
},
{
"carTypeFilter": "GB*",
"count": 8,
"load": "ties",
"direction": 0
},
{
"carTypeFilter": "FM*",
"count": 6,
"load": "rails",
"direction": 0
}
]
}
]
},
"AN_Whittier_Yard_2": {
"identifier": "AN_Whittier_Yard_2",
"name": "Whittier Yard Extension 2",
"groupIds": [
"AN_Whittier_Yard_Yard_Track_3",
"AN_Whittier_Yard_Yard_Track_4",
"AN_Whittier_Yard_Yard_Track_5"
],
"trackSpans": [
"PAN_Whittier_Yard_00"
],
"industryComponent": "AN_Whittier_Yard.yard-site-2",
"area": "whittier",
"description": "An additional 3 tracks for the Whittier yard",
"prerequisiteSections": [
"AN_Whittier_Yard_Sawmill",
"AN_Whittier_Yard_1"
],
"deliveryPhases": [
{
"cost": 2000,
"deliveries": [
{
"carTypeFilter": "GB*",
"count": 8,
"load": "ballast",
"direction": 0
},
{
"carTypeFilter": "GB*",
"count": 12,
"load": "gravel",
"direction": 0
}
]
},
{
"cost": 2000,
"deliveries": [
{
"carTypeFilter": "GB*",
"count": 6,
"load": "gravel",
"direction": 0
},
{
"carTypeFilter": "GB*",
"count": 8,
"load": "ties",
"direction": 0
},
{
"carTypeFilter": "FM*",
"count": 6,
"load": "rails",
"direction": 0
}
]
}
]
},
"AN_Whittier_Yard_3": {
"identifier": "AN_Whittier_Yard_3",
"name": "Whittier Yard Extension 3",
"groupIds": [
"AN_Whittier_Yard_Yard_Track_0",
"AN_Whittier_Yard_Yard_Track_1",
"AN_Whittier_Yard_Yard_Track_2"
],
"trackSpans": [
"PAN_Whittier_Yard_00"
],
"industryComponent": "AN_Whittier_Yard.yard-site-3",
"area": "whittier",
"description": "An additional 3 tracks for the Whittier yard",
"prerequisiteSections": [
"AN_Whittier_Yard_Sawmill",
"AN_Whittier_Yard_1",
"AN_Whittier_Yard_2"
],
"deliveryPhases": [
{
"cost": 2000,
"deliveries": [
{
"carTypeFilter": "GB*",
"count": 8,
"load": "ballast",
"direction": 0
},
{
"carTypeFilter": "GB*",
"count": 12,
"load": "gravel",
"direction": 0
}
]
},
{
"cost": 2000,
"deliveries": [
{
"carTypeFilter": "GB*",
"count": 6,
"load": "gravel",
"direction": 0
},
{
"carTypeFilter": "GB*",
"count": 8,
"load": "ties",
"direction": 0
},
{
"carTypeFilter": "FM*",
"count": 6,
"load": "rails",
"direction": 0
}
]
}
]
},
"AN_Andrews_Interchange_Yard": {
"identifier": "AN_Andrews_Interchange_Yard",
"name": "Andrews Interchange Yard",
"groupIds": [
"AN_Andrews_Interchange_Yard"
],
"description": "A yard that can be useful for organizing east bound trains and storing cars if the Interchange is filled to capacity.",
"trackSpans": [
"PAN_Andrews_Interchange_Yard_00"
],
"industryComponent": "AN_Andrews_Interchange_Yard.interchange-yard-site",
"deliveryPhases": [
{
"cost": 2000,
"deliveries": [
{
"carTypeFilter": "GB*",
"count": 4,
"load": "ballast",
"direction": 0
},
{
"carTypeFilter": "GB*",
"count": 12,
"load": "gravel",
"direction": 0
}
]
},
{
"cost": 2000,
"deliveries": [
{
"carTypeFilter": "GB*",
"count": 2,
"load": "gravel",
"direction": 0
},
{
"carTypeFilter": "GB*",
"count": 8,
"load": "ties",
"direction": 0
},
{
"carTypeFilter": "FM*",
"count": 6,
"load": "rails",
"direction": 0
}
]
}
],
"prerequisiteSections": [
"s6"
],
"area": "andrews"
},
"AN_Alarka_Jct_Additional": {
"identifier": "AN_Alarka_Jct_Additional",
"name": "Alarka Jct Additional Tracks",
"groupIds": [
"AN_Alarka_Jct_Additional"
],
"description": "Additional tracks in Alarka Jct, currently just a bypass around the interchange.",
"prerequisiteSections": [
"alarka-jct"
],
"trackSpans": [
"Pevc"
],
"industryComponent": "AN_Alarka_Jct_Additional.alarka-bypass-site",
"area": "alarka-jct",
"deliveryPhases": [
{
"cost": 2000,
"deliveries": [
{
"carTypeFilter": "GB*",
"count": 4,
"load": "ballast",
"direction": 0
},
{
"carTypeFilter": "GB*",
"count": 2,
"load": "gravel",
"direction": 0
},
{
"carTypeFilter": "GB*",
"count": 2,
"load": "ties",
"direction": 0
},
{
"carTypeFilter": "FM*",
"count": 1,
"load": "rails",
"direction": 0
}
]
}
]
},
"AN_Alarka_Pax_Storage": {
"identifier": "AN_Alarka_Pax_Storage",
"name": "Alarka Pax Storage",
"groupIds": [
"AN_Alarka_Pax_Storage"
],
"description": "Adds two storage tracks to Alarka Station.",
"area": "alarka",
"trackSpans": [
"PAN_Alarka_Pax_Storage_00"
],
"deliveryPhases": [
{
"cost": 2000,
"deliveries": [
{
"carTypeFilter": "GB*",
"count": 2,
"load": "ballast",
"direction": 0
},
{
"carTypeFilter": "GB*",
"count": 2,
"load": "ties",
"direction": 0
},
{
"carTypeFilter": "FM*",
"count": 2,
"load": "rails",
"direction": 0
}
]
}
],
"prerequisiteSections": [
"alarka-branch"
],
"industryComponent": "AN_Alarka_Pax_Storage.alarka-station-site"
},
"AN_Alarka_Loop": {
"identifier": "AN_Alarka_Loop",
"name": "Alarka Balloon Loop",
"groupIds": [
"AN_Alarka_Loop"
],
"description": "Balloon loop in Alarka, no more fighting with a Wye.",
"prerequisiteSections": [
"alarka-branch"
],
"trackSpans": [
"PAN_Alarka_Loop_00"
],
"industryComponent": "AN_Alarka_Loop.alarka-bypass-site",
"area": "alarka",
"deliveryPhases": [
{
"cost": 2000,
"deliveries": [
{
"carTypeFilter": "GB*",
"count": 4,
"load": "ballast",
"direction": 0
},
{
"carTypeFilter": "GB*",
"count": 2,
"load": "gravel",
"direction": 0
},
{
"carTypeFilter": "GB*",
"count": 2,
"load": "ties",
"direction": 0
},
{
"carTypeFilter": "FM*",
"count": 1,
"load": "rails",
"direction": 0
}
]
}
]
}
}
}

View File

@@ -0,0 +1,22 @@
{
"manifestVersion": 5,
"id": "AlinaNova21.AlinasMapMod",
"name": "Alina's Map Mod",
"version": "1.7.25304.436",
"assemblies": [ "AlinasMapMod" ],
"updateUrl": "https://railroader.alinanova.dev/update.json",
"requires": [
{
"id": "railroader",
"notBefore": "2024.6.10"
},
{
"id": "railloader",
"notBefore": "1.10.0.2"
},
{
"id": "Zamu.StrangeCustoms",
"notBefore": "1.10.25017.313"
}
]
}

View File

@@ -0,0 +1,8 @@
# Map Editor Integration
There are new classes adding in AlinasMapMod that allows
itself and other mods to register their objects in MapEditor.
This allows MapEditor to edit and save these objects.
There not much documentation yet, but there is a few implementations in the Loaders, Map, and Station folders. The main interfaces are IEditableObject, ITransformableObject, and IObjectFactory. There is also ICustomHelper for custom helper objects.

View File

@@ -0,0 +1,36 @@
# Pax stations
Industry:
```json
{
"barkers": {
"industries": {
"barkers-station": {
"name": "Barkers Station",
"localPosition": { "x": 0, "y": 0, "z": 0},
"usesContract": false,
"components": {
"ammBarkersStation": {
"name": "Barkers Station",
"type": "AlinasMapMod.PaxStationComponent",
"timetableCode": "BC",
// Reference values: Whittier: 30, Ela: 25, Bryson: 50
"basePopulation": 10,
"loadId": "passengers",
"trackSpans": [ // Spans for loading/unloading
"PAN_Test_Mod_00"
],
// Future support for custom branches, currently supported is "Main" and "Alarka Branch"
"branch": "Main",
// List of ids of other passenger stations.
// Unsure of exact impact
"neighborIds": [],
"carTypeFilter": "*",
"sharedStorage": true
}
}
}
}
}
}
```

View File

@@ -0,0 +1,88 @@
# Progressions
```json
// progressions json format
{
"mapFeatures": {
"sampleMapFeature": {
"displayName": "Sample MapFeature",
"name": "Sample MapFeature",
"description": "Description",
"prerequisites": {
"anotherMapFeature": true
},
"areasEnableOnUnlock": {
"sampleArea": true
},
"defaultEnableInSandbox": false,
"gameObjectsEnableOnUnlock": {
// Existing object in world, primary here for dumps.
"path://scene/world/path/to/gameObject/in/tree": true,
// Requires scenery to be defined in a game-graph
"scenery://sampleSceneryId": true
},
"trackGroupsAvailableOnUnlock": {
"sampleGroup": true
},
"trackGroupsEnableOnUnlock": {
"sampleGroup": true
},
"unlockExcludeIndustries": {
"sampleIndustry": true
},
"unlockIncludeIndustries": {
// Includes components by default
"sampleIndustry": true
},
"unlockIncludeIndustryComponents": {
"sampleIndustryComponent": true
}
}
},
"progressions": {
"ewh": { // ewh is the only current progression tree.
"sections": {
"sampleSection": {
"displayName": "sample milestone",
"description": "Description here",
"prerequisiteSections": {
"anotherSampleSection": true
},
"deliveryPhases": [
// Can have as many phases as you want here.
{
"cost": 1234,
"industryComponent": "sampleIndustryId.componentId",
"deliveries": [
// Can be empty for cost only milestones.
{
"carTypeFilter": "GB*",
"count": 8,
"load": "ballast",
"direction": 0 // 0 = LoadToIndustry, 1 = LoadFromIndustry
},
{
"carTypeFilter": "GB*",
"count": 12,
"load": "gravel",
"direction": 0 // 0 = LoadToIndustry, 1 = LoadFromIndustry
}
]
}
],
// Important note: You cannot both disable and enable the same feature, not even in seperate sections.
"disableFeaturesOnUnlock": {
"sampleMapFeature": true
},
"enableFeaturesOnUnlock": {
"sampleMapFeature": true
},
"enableFeaturesOnAvailable": {
"sampleMapFeature": true
}
}
}
}
}
}
```

158
AlinasMapMod/splineys.md Normal file
View File

@@ -0,0 +1,158 @@
# Splineys
All listed values are defaults, and may usually be omitted unless otherwise noted
- [Prefab Formats](#prefab-formats)
- [Telegraph Poles](#telegraph-poles)
- [Turntables](#turntables)
- [Loaders](#loaders)
- [Passenger Station Agent](#passenger-station-agent-includes-building)]
- [Passenger Stations](#passenger-stations)
- [Map Labels](#map-labels)
## Prefab formats
All prefabs use the following formats:
- Path based: `path://scene/world/path/to/gameObject/in/tree`
- Scenery: `scenery://sampleSceneryId`
- Vanilla: `vanilla://vanillaObjectid`
This is a special one for specific structures,
as of 05/10/2025, it has the following:
- roundhouseStall
- roundhouseStart
- roundhouseEnd
- coalConveyor
- coalTower
- dieselFuelingStand
- waterTower
- waterColumn
- flagStopStation
- brysonDepot
- dillsboroStation
- southernCombinationDepot
## Telegraph poles
```json
{
"handler": "AlinasMapMod.TelegraphPoleBuilder",
"polesToRaise": [1,2,3]
}
```
```json
{
"handler": "AlinasMapMod.TelegraphPoleMover",
"polesToMove": [1,2,3],
"poleMovement": [
[0,1,2],
[22,2,0],
[33,3,5],
]
}
```
## Turntables
```json
{
"handler":"AlinasMapMod.Turntable.TurntableBuilder",
"radius": 15,
"subdivisions": 32,
"position": { "x": 0, "y": 0, "z": 0 },
"rotation": { "x": 0, "y": 0, "z": 0 },
"roundhouseStalls": 0,
"roundhouseTrackLength": 46,
"stallPrefab": "vanilla://roundhouseStall",
"startPrefab": "vanilla://roundhouseStart",
"endPrefab": "vanilla://roundhouseEnd"
}
```
## Loaders
```json
{
"handler": "AlinasMapMod.LoaderBuilder",
"position": { "x": 0, "y": 0, "z": 0 },
"rotation": { "x": 0, "y": 0, "z": 0 },
// Must be set to one of the coal, diesel, or water prefabs
"prefab": "empty://",
// Required for coal and diesel, see example industry below
"industry": "",
}
```
Example industry:
```json
{
"loader-industry-example": {
"name": "Example industry for loaders",
"localPosition": { "x": -271.6577, "y": 0.0, "z": -22.8286133 },
"usesContract": false,
"components": {
"coaling": {
"type": "Model.Ops.IndustryUnloader",
"name": "Example Coaling Tower",
"trackSpans": [ "PExampleSpan" ],
"carTypeFilter": "HM,HT",
"sharedStorage": true,
"loadId": "coal",
"storageChangeRate": 0.0,
"maxStorage": 300000.0,
"orderAroundEmpties": false,
"carTransferRate": 1E+07,
"orderAroundLoaded": false
},
"diesel": {
"type": "Model.Ops.IndustryUnloader",
"name": "Example Diesel Stand",
"trackSpans": [ "PExampleSpan" ],
"carTypeFilter": "TM",
"sharedStorage": true,
"loadId": "diesel-fuel",
"storageChangeRate": 0.0,
"maxStorage": 16000.0,
"orderAroundEmpties": false,
"carTransferRate": 32000.0,
"orderAroundLoaded": false
}
}
}
}
```
## Passenger Station Agent (Includes building)
```json
{
"handler": "AlinasMapMod.StationAgentBuilder",
"position": {
"x": 12886,
"y": 562,
"z": 4703
},
"rotation": {
"x": 0,
"y": 0,
"z": 0
},
"prefab": "vanilla://flagStopStation",
"passengerStop": "whittier"
}
```
## Passenger Stations
### <span style="color:red">***DEPRECATED: Use the industry component instead***
## Map Labels
### Note that live editing may have ssizing issues, these are usually resolved on a save reload.
```json
{
"handler": "AlinasMapMod.Map.MapLabelBuilder",
"position": { "x": 0, "y": 0, "z": 0 },
"text": "Map Label",
}
```

View File

@@ -0,0 +1,861 @@
# Validation Framework Documentation
## Overview
The AlinasMapMod validation framework provides a standardized approach to validating spliney builder components and serialized objects. This framework uses a field-based validation system with fluent API extensions and comprehensive error reporting.
## Core Architecture
### ValidationBuilder<T> (Field-Based Validation)
The validation system is built around `ValidationBuilder<T>` which requires a field name for context:
```csharp
// Create a validation builder for a specific field
var builder = new ValidationBuilder<string>("PrefabUri");
// Add validation rules using fluent API
builder.AsUri()
.AsVanillaPrefab(allowedPrefabs);
// Validate a value
var result = builder.Validate("vanilla://locomotive_steam_small");
```
### ValidationResult System
The framework provides detailed validation feedback with manual result creation:
```csharp
public class ValidationResult
{
public bool IsValid { get; set; } = true;
public List<ValidationError> Errors { get; set; } = new List<ValidationError>();
public List<ValidationWarning> Warnings { get; set; } = new List<ValidationWarning>();
public void ThrowIfInvalid(); // Throws ValidationException if invalid
// Combines multiple validation results with null handling
public static ValidationResult Combine(params ValidationResult[] results);
}
```
### ValidationError and ValidationWarning
Detailed error information with context:
```csharp
public class ValidationError
{
public string Field { get; set; } // Field name being validated
public string Message { get; set; } // Human-readable error message
public string Code { get; set; } // Machine-readable error code
public object Value { get; set; } // The invalid value
}
public class ValidationWarning
{
public string Field { get; set; }
public string Message { get; set; }
public object Value { get; set; }
}
```
## Validation Rules
### Core Validation Rules
- **RequiredRule<T>**: Validates non-null/non-empty values with whitespace control
- **WhitelistRule<T>**: Validates values against allowed lists (includes null validation)
- **MinValueRule<T>**: Validates minimum values for comparable types
- **EnumValidationRule<TEnum>**: Validates enum values are defined
- **CustomRule<T>**: Allows custom validation logic
### URI Validation Rules
#### Basic URI Rules
- **UriFormatRule**: Validates URI format (contains "://")
- **UriSchemeValidationRule**: Validates supported schemes (case-insensitive: path, scenery, vanilla, empty)
#### Scheme-Specific Rules (Restrictive)
- **VanillaPrefabRule**: Only allows `vanilla://` URIs, validates prefab names with path extraction
- **PathUriRule**: Only allows `path://` URIs, validates scene path format
- **SceneryUriRule**: Only allows `scenery://` URIs, rejects identifiers with paths
- **EmptyUriRule**: Only allows `empty://` URIs or null/empty values
#### Comprehensive Rules (Permissive)
- **GameObjectUriRule**: Allows multiple URI schemes, validates each according to its rules
### Cache Validation Rules
- **CacheValidationRule<T>**: Validates cache key existence with null handling and exception safety
- **CacheTypeValidationRule<TExpected>**: Validates cached object types with comprehensive error handling
## Builder Standardization
### SplineyBuilderBase Class
All builders inherit from `SplineyBuilderBase` which provides standardized patterns for logging, error handling, validation, and object creation:
```csharp
public abstract class SplineyBuilderBase : StrangeCustoms.ISplineyBuilder
{
protected static readonly Serilog.ILogger Logger = Log.ForContext<SplineyBuilderBase>();
// Main build method with standardized error handling
public GameObject BuildSpliney(string id, Transform parentTransform, JObject data)
{
return SafeBuildSpliney(id, parentTransform, data, () => BuildSplineyInternal(id, parentTransform, data));
}
// To be implemented by derived classes
protected abstract GameObject BuildSplineyInternal(string id, Transform parentTransform, JObject data);
}
```
### Core Builder Features
#### Input Validation
```csharp
protected virtual void ValidateInput(string id, JObject data)
{
if (string.IsNullOrEmpty(id))
throw new ValidationException("ID cannot be null or empty");
if (data == null)
throw new ValidationException("Data cannot be null");
}
```
#### GameObject Creation
```csharp
protected virtual GameObject CreateGameObject(string id, Transform parentTransform, string objectTypeName = null)
{
var gameObject = new GameObject(id);
if (parentTransform != null)
{
gameObject.transform.SetParent(parentTransform);
}
else
{
var parent = Utils.GetParent(objectTypeName ?? "Unknown");
if (parent != null)
gameObject.transform.SetParent(parent.transform);
}
return gameObject;
}
```
#### Standardized Error Handling
Safe execution with structured logging and error handling:
```csharp
protected GameObject SafeBuildSpliney(string id, Transform parentTransform, JObject data, Func<GameObject> buildAction)
{
try
{
ValidateInput(id, data);
Logger.Information("Building {BuilderType} with ID {Id}", GetType().Name, id);
var result = buildAction();
Logger.Information("Successfully built {BuilderType} with ID {Id}", GetType().Name, id);
return result;
}
catch (ValidationException ex)
{
Logger.Error(ex, "Validation failed for {BuilderType} {Id}", GetType().Name, id);
throw new ValidationException($"Validation failed for {GetType().Name} {id}: {ex.Message}", ex);
}
catch (Exception ex)
{
Logger.Error(ex, "Failed to create {BuilderType} {Id}", GetType().Name, id);
throw new InvalidOperationException($"Failed to create {GetType().Name} {id}", ex);
}
}
```
#### Transactional Building with Cleanup
For builders that need cleanup on failure:
```csharp
protected GameObject SafeBuildSplineyWithCleanup(string id, Transform parentTransform, JObject data, Func<IBuilderTransaction, GameObject> buildAction)
{
var transaction = new BuilderTransaction();
try
{
ValidateInput(id, data);
Logger.Information("Building {BuilderType} with ID {Id}", GetType().Name, id);
var result = buildAction(transaction);
transaction.Commit();
Logger.Information("Successfully built {BuilderType} with ID {Id}", GetType().Name, id);
return result;
}
catch (Exception ex)
{
transaction.Rollback();
Logger.Error(ex, "Failed to create {BuilderType} {Id}", GetType().Name, id);
throw;
}
}
```
#### Deserialization and Validation
Helper method for deserializing and validating data objects:
```csharp
protected TData DeserializeAndValidate<TData>(JObject data) where TData : IValidatable
{
var deserializedData = data.ToObject<TData>();
if (deserializedData == null)
throw new ValidationException($"Failed to deserialize data as {typeof(TData).Name}");
// Use rich validation for detailed error reporting
var validationResult = deserializedData.ValidateWithDetails();
if (!validationResult.IsValid)
{
foreach (var error in validationResult.Errors)
{
Logger.Error("Validation error in {Field}: {Message}", error.Field, error.Message);
}
}
// Log warnings but continue
foreach (var warning in validationResult.Warnings)
{
Logger.Warning("Validation warning in {Field}: {Message}", warning.Field, warning.Message);
}
validationResult.ThrowIfInvalid();
return deserializedData;
}
```
#### Component Building Pattern
For building from serialized components that implement `ICreatableComponent`:
```csharp
protected virtual GameObject BuildFromCreatableComponent<TComponent, TSerialized>(string id, JObject data)
where TComponent : Component
where TSerialized : ICreatableComponent<TComponent>, IValidatable
{
var serialized = DeserializeAndValidate<TSerialized>(data);
Logger.Information("Creating {ObjectType} {Id} using serialized component", typeof(TComponent).Name, id);
return serialized.Create(id).gameObject;
}
```
## Serialized Component Framework
### SerializedComponentBase<T>
Serialized components can inherit from `SerializedComponentBase<T>` for standardized validation functionality:
```csharp
public abstract class SerializedComponentBase<T> : ISerializedPatchableComponent<T>, IValidatable
where T : Component
{
private readonly Dictionary<string, Func<ValidationResult>> _validators = new Dictionary<string, Func<ValidationResult>>();
private bool _validationConfigured = false;
// Override to configure validation rules
protected virtual void ConfigureValidation() { }
// Abstract methods to be implemented by derived classes
public abstract T Create(string id);
public abstract void Write(T comp);
public abstract void Read(T comp);
// IValidatable implementation
public virtual void Validate() => ValidateWithDetails().ThrowIfInvalid();
public virtual ValidationResult ValidateWithDetails() { /* implementation */ }
}
```
### Property-Based Validation Rules
Use the `RuleFor` method to create validation rules for properties:
```csharp
protected ValidationBuilder<TValue> RuleFor<TValue>(Expression<Func<TValue>> propertyExpression)
{
// Creates a ValidationBuilder for the specified property
var propertyName = GetPropertyName(propertyExpression);
var builder = new ValidationBuilder<TValue>(propertyName);
// Store validator function for later execution
_validators[propertyName] = () => {
var propertyValue = GetPropertyValue<TValue>(propertyExpression);
var context = new ValidationContext { Owner = this };
return builder.Validate(propertyValue, context);
};
return builder;
}
```
### Example Implementation
```csharp
public class SerializedLoader : SerializedComponentBase<LoaderInstance>
{
public string Prefab { get; set; }
public int Capacity { get; set; }
public Vector3 Position { get; set; }
protected override void ConfigureValidation()
{
RuleFor(() => Prefab)
.Required()
.AsUri()
.AsVanillaPrefab(VanillaPrefabs.AvailableLoaderPrefabs);
RuleFor(() => Capacity)
.GreaterThan(0);
}
public override LoaderInstance Create(string id)
{
// Create and configure the component
var gameObject = new GameObject(id);
var loader = gameObject.AddComponent<LoaderInstance>();
// Apply configuration
loader.SetPrefab(Prefab);
loader.SetCapacity(Capacity);
gameObject.transform.position = Position;
return loader;
}
public override void Write(LoaderInstance comp)
{
Prefab = comp.GetPrefab();
Capacity = comp.GetCapacity();
Position = comp.transform.position;
}
public override void Read(LoaderInstance comp)
{
comp.SetPrefab(Prefab);
comp.SetCapacity(Capacity);
comp.transform.position = Position;
}
}
```
### Fluent Validation API
The framework provides a fluent API through extension methods on `ValidationBuilder<T>`:
```csharp
var builder = new ValidationBuilder<string>("PrefabField");
// Basic validation
builder.Required() // Value must not be null/empty
.AsUri(); // Must contain "://"
// URI scheme validation
builder.AsUriScheme() // Must be supported scheme
.AsVanillaPrefab(allowedPrefabs) // Specific to vanilla:// URIs
.AsPathUri() // Specific to path:// URIs
.AsSceneryUri() // Specific to scenery:// URIs
.AsEmptyUri() // Specific to empty:// URIs
.AsGameObjectUri(allowedPrefabs); // Multiple URI forms allowed
// Numeric validation
builder.GreaterThan(0) // Value > minimum
.GreaterThanOrEqual(0); // Value >= minimum
// Enum validation
builder.AsValidEnum<MyEnum>(); // Must be defined enum value
// Custom validation
builder.Custom((value, context) => { // Custom validation logic
var result = new ValidationResult { IsValid = true };
if (/* custom condition */) {
result.IsValid = false;
result.Errors.Add(new ValidationError {
Field = context.FieldName,
Message = "Custom validation failed",
Code = "CUSTOM_ERROR",
Value = value
});
}
return result;
});
```
## Complete Builder Implementation Example
### Creating a New Builder
Here's how to implement a complete builder following the standardized patterns:
```csharp
public class MyCustomBuilder : SplineyBuilderBase, IObjectFactory
{
public string Name => "My Custom Component";
public bool Enabled => true;
public Type ObjectType => typeof(MyCustomComponent);
protected override GameObject BuildSplineyInternal(string id, Transform parentTransform, JObject data)
{
// Use the standardized pattern for building from serialized components
return BuildFromCreatableComponent<MyCustomComponent, SerializedMyCustomComponent>(id, data);
}
public IEditableObject CreateObject(PatchEditor editor, string id)
{
// Create a default instance for the editor
return new SerializedMyCustomComponent
{
Prefab = "vanilla://default_prefab",
Scale = 1.0f
}.Create(id);
}
}
```
### Corresponding Serialized Component
```csharp
public class SerializedMyCustomComponent : SerializedComponentBase<MyCustomComponent>
{
public string Prefab { get; set; }
public float Scale { get; set; } = 1.0f;
public bool IsActive { get; set; } = true;
protected override void ConfigureValidation()
{
RuleFor(() => Prefab)
.Required()
.AsUri()
.AsVanillaPrefab(VanillaPrefabs.AvailableComponentPrefabs);
RuleFor(() => Scale)
.GreaterThan(0.1f);
}
public override MyCustomComponent Create(string id)
{
var gameObject = CreateGameObject(id);
var component = GetOrAddComponent<MyCustomComponent>(gameObject);
ConfigureWithActivation(gameObject, () => {
component.SetPrefab(Prefab);
component.SetScale(Scale);
component.SetActive(IsActive);
});
return component;
}
public override void Write(MyCustomComponent comp)
{
Prefab = comp.GetPrefab();
Scale = comp.GetScale();
IsActive = comp.IsActive();
}
public override void Read(MyCustomComponent comp)
{
comp.SetPrefab(Prefab);
comp.SetScale(Scale);
comp.SetActive(IsActive);
}
private GameObject CreateGameObject(string id)
{
var gameObject = new GameObject(id);
return gameObject;
}
private TComponent GetOrAddComponent<TComponent>(GameObject gameObject) where TComponent : Component
{
return gameObject.GetComponent<TComponent>() ?? gameObject.AddComponent<TComponent>();
}
private void ConfigureWithActivation(GameObject gameObject, System.Action configureAction)
{
var wasActive = gameObject.activeSelf;
gameObject.SetActive(false);
try
{
configureAction();
}
finally
{
gameObject.SetActive(wasActive);
}
}
}
```
## Migration Guide
### Updating Existing Builders
1. **Inherit from SplineyBuilderBase**: Replace custom base classes with `SplineyBuilderBase`
2. **Use BuildFromCreatableComponent**: Replace manual deserialization with the standardized pattern
3. **Implement IObjectFactory**: Add `CreateObject` method for editor integration
4. **Remove custom logging**: Use the inherited `Logger` instance
**Before:**
```csharp
public class OldBuilder : ISplineyBuilder
{
public GameObject BuildSpliney(string id, Transform parent, JObject data)
{
try
{
var serialized = data.ToObject<SerializedComponent>();
serialized.Validate(); // Simple validation
return serialized.Create(id).gameObject;
}
catch (Exception ex)
{
Console.WriteLine($"Error: {ex.Message}");
throw;
}
}
}
```
**After:**
```csharp
public class NewBuilder : SplineyBuilderBase, IObjectFactory
{
public string Name => "My Component";
public bool Enabled => true;
public Type ObjectType => typeof(MyComponent);
protected override GameObject BuildSplineyInternal(string id, Transform parentTransform, JObject data)
{
return BuildFromCreatableComponent<MyComponent, SerializedComponent>(id, data);
}
public IEditableObject CreateObject(PatchEditor editor, string id) =>
new SerializedComponent().Create(id);
}
```
### Updating Serialized Components
1. **Inherit from SerializedComponentBase<T>**: Replace custom validation with the standardized framework
2. **Override ConfigureValidation**: Use `RuleFor` to set up validation rules
3. **Use ValidationBuilder**: Replace manual validation with fluent API
**Before:**
```csharp
public class OldSerializedComponent : IValidatable
{
public string Prefab { get; set; }
public void Validate()
{
if (string.IsNullOrEmpty(Prefab))
throw new ValidationException("Prefab is required");
}
public ValidationResult ValidateWithDetails()
{
// Manual validation result creation
}
}
```
**After:**
```csharp
public class NewSerializedComponent : SerializedComponentBase<MyComponent>
{
public string Prefab { get; set; }
protected override void ConfigureValidation()
{
RuleFor(() => Prefab)
.Required()
.AsUri()
.AsVanillaPrefab(allowedPrefabs);
}
// Implement Create, Write, Read methods
}
```
## Builder Helper Methods
The `SplineyBuilderBase` class provides additional helper methods for common builder tasks:
### Finding Existing Components
```csharp
// Find existing components by ID using caches
protected virtual TComponent FindExistingComponent<TComponent>(string id) where TComponent : Component
{
// Uses type-specific caches for performance
if (typeof(TComponent) == typeof(PaxStationAgent))
{
StationAgentCache.Instance.TryGetValue(id, out var stationAgent);
return stationAgent as TComponent;
}
// Fallback to GameObject.FindObjectsOfType
return GameObject.FindObjectsOfType<TComponent>(true)
.FirstOrDefault(c => GetComponentId(c) == id);
}
```
### Component ID Resolution
```csharp
// Gets component ID using common patterns
protected virtual string GetComponentId(Component component)
{
// Try common identifier patterns
var identifiableProperty = component.GetType().GetProperty("identifier");
if (identifiableProperty != null)
return identifiableProperty.GetValue(component)?.ToString();
var idProperty = component.GetType().GetProperty("Id");
if (idProperty != null)
return idProperty.GetValue(component)?.ToString();
return component.name;
}
```
### Safe Component Configuration
```csharp
// Safely configure GameObject by disabling during configuration
protected virtual void ConfigureWithActivation(GameObject gameObject, System.Action configureAction)
{
var wasActive = gameObject.activeSelf;
gameObject.SetActive(false);
try
{
configureAction();
}
finally
{
gameObject.SetActive(wasActive);
}
}
```
### Validation Patterns
#### Field-Based Validation
```csharp
// Validate a URI field
var uriBuilder = new ValidationBuilder<string>("PrefabUri");
uriBuilder.Required()
.AsUri()
.AsVanillaPrefab(VanillaPrefabs.AvailableLoaderPrefabs);
var uriResult = uriBuilder.Validate("vanilla://locomotive_steam_small");
if (!uriResult.IsValid)
{
// Handle validation errors
foreach (var error in uriResult.Errors)
{
Logger.Error("Validation error in {Field}: {Message}", error.Field, error.Message);
}
}
// Validate numeric values
var numberBuilder = new ValidationBuilder<int>("Subdivisions");
numberBuilder.GreaterThan(2);
var numberResult = numberBuilder.Validate(subdivisions);
```
#### Creating Validation Rules
```csharp
// Create a custom validation rule
public class PositiveNumberRule : ValidationRule<int>
{
public override ValidationResult Validate(int value, ValidationContext context)
{
var result = new ValidationResult { IsValid = true };
if (value <= 0)
{
result.IsValid = false;
result.Errors.Add(new ValidationError
{
Field = context.FieldName,
Message = $"Value must be positive, got {value}",
Code = "MUST_BE_POSITIVE",
Value = value
});
}
return result;
}
}
// Use the custom rule
var builder = new ValidationBuilder<int>("MyNumber");
builder.AddRule(new PositiveNumberRule());
```
#### Working with ValidationContext
```csharp
var context = new ValidationContext { FieldName = "PrefabField" };
var rule = new RequiredRule<string>();
var result = rule.Validate(value, context);
// The context provides field information for error messages
```
## URI Validation Architecture
### Two Types of URI Rules
The validation framework supports two distinct approaches to URI validation:
#### Restrictive Rules (Single Scheme)
These rules only accept their specific URI scheme and reject all others:
- **VanillaPrefabRule**: Only accepts `vanilla://` URIs
- **PathUriRule**: Only accepts `path://` URIs
- **SceneryUriRule**: Only accepts `scenery://` URIs
- **EmptyUriRule**: Only accepts `empty://` URIs
```csharp
// This will ONLY accept vanilla:// URIs
var vanillaBuilder = new ValidationBuilder<string>("VanillaPrefab");
vanillaBuilder.AsVanillaPrefab(allowedPrefabs);
// VALID: "vanilla://locomotive_steam_small"
// INVALID: "path://scene/SomeObject"
```
#### Permissive Rules (Multiple Schemes)
These rules accept multiple URI schemes and validate each according to its rules:
- **GameObjectUriRule**: Accepts path://, scenery://, vanilla://, empty:// schemes
```csharp
// This will accept multiple URI forms
var gameObjectBuilder = new ValidationBuilder<string>("GameObjectUri");
gameObjectBuilder.AsGameObjectUri(allowedVanillaPrefabs);
// VALID: "vanilla://locomotive_steam_small"
// VALID: "path://scene/SomeObject"
// VALID: "scenery://tree_oak"
// VALID: "empty://"
```
### URI Parsing Details
#### Vanilla Prefab Extraction
The `VanillaPrefabRule` extracts prefab names from URIs with paths:
```csharp
// Input: "vanilla://Locomotive/part"
// Extracted prefab: "Locomotive"
// Validation: Check if "Locomotive" is in allowed prefabs list
```
#### Path URI Format
Path URIs must follow the format `path://scene/GameObject1/GameObject2/...`:
```csharp
// VALID: "path://scene/SomeGameObject"
// VALID: "path://scene/Parent/Child"
// INVALID: "path://notscene/SomeObject"
```
#### Scenery URI Format
Scenery URIs must be simple identifiers without paths:
```csharp
// VALID: "scenery://tree_oak"
// INVALID: "scenery://category/tree_oak" // Contains path separator
```
### Case Sensitivity
URI scheme validation is case-insensitive:
```csharp
// All of these are equivalent:
// "PATH://scene/Object"
// "path://scene/Object"
// "Path://scene/Object"
```
## Common Error Codes
The validation framework uses standardized error codes for consistent error handling:
### Core Validation Errors
- `REQUIRED`: Value is required but null/empty
- `REQUIRED_NOT_WHITESPACE`: Value cannot be whitespace-only
- `INVALID_VALUE`: Value not in allowed whitelist
- `MIN_VALUE_VIOLATION`: Value below minimum threshold
- `INVALID_ENUM_VALUE`: Enum value not defined
### URI Validation Errors
- `INVALID_URI_FORMAT`: URI missing "://" separator
- `INVALID_URI_SCHEME`: Unsupported URI scheme
- `NON_VANILLA_URI_NOT_ALLOWED`: Non-vanilla URI in vanilla-only context
- `NON_PATH_URI_NOT_ALLOWED`: Non-path URI in path-only context
- `NON_SCENERY_URI_NOT_ALLOWED`: Non-scenery URI in scenery-only context
- `NON_EMPTY_URI_NOT_ALLOWED`: Non-empty URI in empty-only context
- `INVALID_VANILLA_PREFAB`: Vanilla prefab not in allowed list
- `INVALID_PATH_URI_FORMAT`: Path URI format incorrect
- `MISSING_PATH_COMPONENTS`: Path URI missing GameObject path
- `MISSING_SCENERY_IDENTIFIER`: Scenery URI missing identifier
- `SCENERY_IDENTIFIER_CONTAINS_PATH`: Scenery identifier contains path separators
- `VALUE_MUST_BE_EMPTY`: Value must be empty or null
### Cache Validation Errors
- `CACHE_KEY_NULL_OR_EMPTY`: Cache key cannot be null or empty
- `CACHE_NOT_FOUND`: Cache key not found
- `CACHE_ACCESS_ERROR`: Error accessing cache
- `CACHE_OBJECT_NULL`: Cached object is null
- `INVALID_CACHE_OBJECT_TYPE`: Cached object has wrong type
## Benefits
1. **Field-Based Context**: Validation tied to specific fields provides clear error context
2. **Comprehensive Error Reporting**: Detailed error messages with codes, fields, and values
3. **Flexible Architecture**: Support for both restrictive and permissive validation patterns
4. **URI Scheme Awareness**: Intelligent handling of different URI schemes and formats
5. **Exception Safety**: Proper null handling and exception management
6. **Extensibility**: Easy to add new validation rules through inheritance
7. **Performance**: Efficient validation with early returns and minimal overhead
8. **Testability**: Comprehensive test coverage ensures reliability
## Migration Notes
When updating validation code:
1. **ValidationBuilder Constructor**: Always provide a field name
2. **Manual ValidationResult Creation**: No static `Valid`/`Invalid` properties
3. **URI Rule Selection**: Choose restrictive vs permissive rules based on requirements
4. **Null Handling**: Validation rules now properly handle null values
5. **Error Codes**: Use standardized error codes for consistent error handling
## Best Practices
1. **Use Descriptive Field Names**: `"PrefabUri"` instead of `"field1"`
2. **Chain Validation Rules**: Use fluent API for multiple validation rules
3. **Handle ValidationResult**: Always check `IsValid` before proceeding
4. **Log Validation Errors**: Use structured logging with error context
5. **Choose Appropriate URI Rules**: Restrictive for specific schemes, permissive for flexibility
6. **Test Validation Logic**: Write tests for both valid and invalid cases