Add Alina’s Map Mod 1.7.25304.436
https://rmh.alinanova.dev/mod/cba5981c-8e8e-4b01-918c-214fa155adff
This commit is contained in:
BIN
AlinasMapMod/AlinasMapMod.dll
Normal file
BIN
AlinasMapMod/AlinasMapMod.dll
Normal file
Binary file not shown.
636
AlinasMapMod/AlinasMapMod.json
Normal file
636
AlinasMapMod/AlinasMapMod.json
Normal 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
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
22
AlinasMapMod/Definition.json
Normal file
22
AlinasMapMod/Definition.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
8
AlinasMapMod/mapeditor_integration.md
Normal file
8
AlinasMapMod/mapeditor_integration.md
Normal 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.
|
||||
|
||||
36
AlinasMapMod/paxstations.md
Normal file
36
AlinasMapMod/paxstations.md
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
88
AlinasMapMod/progressions.md
Normal file
88
AlinasMapMod/progressions.md
Normal 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
158
AlinasMapMod/splineys.md
Normal 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",
|
||||
}
|
||||
|
||||
```
|
||||
861
AlinasMapMod/validation_framework.md
Normal file
861
AlinasMapMod/validation_framework.md
Normal 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
|
||||
Reference in New Issue
Block a user