diff --git a/src/helpers/angleHelper.js b/src/helpers/angleHelper.js index a435e96..2dbda25 100644 --- a/src/helpers/angleHelper.js +++ b/src/helpers/angleHelper.js @@ -1,3 +1,5 @@ +import Vector3 from "../model/vector3"; + function degToRad(angle) { return angle * (Math.PI / 180); } @@ -16,10 +18,19 @@ function normalizeDegAngle(angle) { } } +function normalizeDegVector(vector) { + return new Vector3( + normalizeDegAngle(vector.x), + normalizeDegAngle(vector.y), + normalizeDegAngle(vector.z) + ) +} + const AngleHelper = { degToRad, radToDeg, normalizeDegAngle, + normalizeDegVector } export default AngleHelper; \ No newline at end of file diff --git a/src/helpers/constants.js b/src/helpers/constants.js index a473f20..9d4b15e 100644 --- a/src/helpers/constants.js +++ b/src/helpers/constants.js @@ -20,6 +20,7 @@ const Constants = { attachSignsMaxDistanceX: 0.2, attachSignsGridSize: 10, logAttachedSigns: false, + skipBaseMisc: true }, warnings: { all: false, // enable all warnings @@ -37,9 +38,9 @@ const Constants = { invalidSceneryInfoVersion: true, signUndefinedPrefabName: true, tracksConnectionTest: true, + tracksConnectionTestSwitchRelated: true, electrificationNevpNotApplied: false, electrificationConflict: false, - tracksConnectionTestSwitchRelated: true, electrificationResolverError: true, electrificationResolverWarnings: true, signalElemsUnknownPrefab: true, @@ -50,6 +51,7 @@ const Constants = { connectTracksFailed: true, routeInvalidSegment: true, derailerUnknownDirection: true, + endMiscGroupWithoutStart: true, }, errors: { invalidSceneryInfo: true diff --git a/src/model/misc-group.js b/src/model/misc-group.js new file mode 100644 index 0000000..9695270 --- /dev/null +++ b/src/model/misc-group.js @@ -0,0 +1,21 @@ +import Vector3 from "./vector3"; + +export default class MiscGroup { + pos; + rot; + + constructor(pos, rot) { + this.pos = pos; + this.rot = rot; + } + + static fromText(text) { + const values = text.split(";"); + const object = new MiscGroup( + Vector3.fromValuesArray(values, 3), // pos + Vector3.fromValuesArray(values, 6), // rot + ); + + return object; + } +} diff --git a/src/model/misc/misc.js b/src/model/misc/misc.js index c1a1ab1..93a28bc 100644 --- a/src/model/misc/misc.js +++ b/src/model/misc/misc.js @@ -1,3 +1,4 @@ +import AngleHelper from "../../helpers/angleHelper"; import SceneryObject from "../scenery-object"; import Vector3 from "../vector3"; @@ -6,7 +7,6 @@ export default class Misc extends SceneryObject { name; category = "misc"; type = "Misc"; - applied = false; constructor(id, misc_id, prefab_name, pos, rot, name) { super(id, pos, rot); @@ -17,17 +17,37 @@ export default class Misc extends SceneryObject { }); } - static fromText(id, text) { + static fromText(id, text, miscGroups = []) { const values = text.split(";"); + const object = new Misc( id, // id values[1], // misc_id values[2], // prefab_name - Vector3.fromValuesArray(values, 3), // pos - Vector3.fromValuesArray(values, 6), // rot + ...Misc.applyGroupTransforms( + Vector3.fromValuesArray(values, 3), // pos + Vector3.fromValuesArray(values, 6), // rot + miscGroups + ), values[9] // name ); return object; } + + static applyGroupTransforms(pos, rot, miscGroups) { + if(!miscGroups) return [ pos, rot ]; + + for (const group of miscGroups) { + const rotRad = AngleHelper.degToRad(group.rot.y); + const rotRadY = new Vector3(0, rotRad, 0); + pos = pos.rotate(rotRadY).add(group.pos); + rot = rot.add(group.rot); + // TODO: Replace this with some fancy quaternion math maybe + } + + rot = AngleHelper.normalizeDegVector(rot); + + return [ pos, rot ]; + } } diff --git a/src/model/misc/signalbox.js b/src/model/misc/signalbox.js index 5070c3b..16966cf 100644 --- a/src/model/misc/signalbox.js +++ b/src/model/misc/signalbox.js @@ -25,14 +25,21 @@ export default class SignalBox extends Misc { scenery.addSignalBox(this); } - static fromText(id, text) { + static isSignalBox(prefabName) { + return prefabName.startsWith('SignalBox'); + } + + static fromText(id, text, miscGroups = []) { const values = text.split(";"); const object = new SignalBox( id, // id values[1], // misc_id values[2], // prefab_name - Vector3.fromValuesArray(values, 3), // pos - Vector3.fromValuesArray(values, 6), // rot + ...Misc.applyGroupTransforms( + Vector3.fromValuesArray(values, 3), // pos + Vector3.fromValuesArray(values, 6), // rot + miscGroups + ), values[9] // name ); diff --git a/src/model/scenery-parser.js b/src/model/scenery-parser.js index bf100f8..e3832cf 100644 --- a/src/model/scenery-parser.js +++ b/src/model/scenery-parser.js @@ -19,17 +19,18 @@ import Derailer from './track-objects/derailer'; import SpawnPoint from './track-objects/spawn-point'; import { attachSigns } from './attach-signs'; import {connectTracks} from "./connect-tracks"; - -/* -TODO: add support for WorldRotation and WorldTranslation -*/ +import MiscGroup from './misc-group'; export default class SceneryParser { + static nextMiscId = 1; + static fromText(scText) { + SceneryParser.nextMiscId = 1; const lines = scText.split("\n").map(line => line.trim()); const scenery = new Scenery(); let currentRoute = null; + const currentMiscGroups = []; lines.forEach((line, index) => { if(index === 0) { @@ -37,22 +38,30 @@ export default class SceneryParser { if(sceneryInfo) scenery.addObject(sceneryInfo); return; } + if(!line?.length) return; if (currentRoute !== null) { - const foundRouteEnd = SceneryParser._parseRouteLine(line, currentRoute); - if (foundRouteEnd) { + const isRouteEnd = SceneryParser._parseRouteLine(line, currentRoute); + + if (isRouteEnd) { scenery.addObject(currentRoute); currentRoute = null; } - } else { - const object = SceneryParser._parseObject(line); - if(!object) return; // skip null objects - if (object.type === 'Route') { + + return; + } + + const object = SceneryParser._parseObject(line, currentMiscGroups); + if(!object) return; // skip null objects + + switch(object.type) { + case 'Route': currentRoute = object; - } else { + return; // do not add Route object yet + default: scenery.addObject(object); - } + break; } }); @@ -80,7 +89,7 @@ export default class SceneryParser { return SceneryInfo.fromText(text); } - static _parseObject(text) { + static _parseObject(text, currentMiscGroups) { const type = text.split(";", 2)[0]; if (type === 'EndRoute' || type.indexOf("Forest") !== -1 || type.indexOf("Empty") !== -1) { @@ -98,13 +107,22 @@ export default class SceneryParser { case 'Route': return Route.fromText(text); case 'Misc': - return SceneryParser._parseMisc(text); + return SceneryParser._parseMisc(text, currentMiscGroups); case 'CameraHome': return CameraHome.fromText(text); case 'MainCamera': return MainCamera.fromText(text); case 'MiscGroup': + const group = MiscGroup.fromText(text); + currentMiscGroups.unshift(group); + + return null; case 'EndMiscGroup': + if(!currentMiscGroups.shift()) { + SceneryParserLog.warn('endMiscGroupWithoutStart', 'Unexpected EndMiscGroup found without a preceding MiscGroup'); + } + + return null; case 'Wires': case 'Fence': case 'TerrainPoint': @@ -152,13 +170,16 @@ export default class SceneryParser { } } - static _parseMisc(text) { + static _parseMisc(text, miscGroups) { const prefabName = text.split(';', 4)[2]; + const id = SceneryParser.nextMiscId++; - if(prefabName.startsWith("SignalBox")) { - return SignalBox.fromText(Scenery.nextMiscId++, text); + if(SignalBox.isSignalBox(prefabName)) { + return SignalBox.fromText(id, text, miscGroups); + } else if(Constants.parser.skipBaseMisc) { + return null; } else { - return Misc.fromText(Scenery.nextMiscId++, text); + return Misc.fromText(id, text, miscGroups); } } diff --git a/src/model/scenery.js b/src/model/scenery.js index b3a69d5..f003ff6 100644 --- a/src/model/scenery.js +++ b/src/model/scenery.js @@ -7,7 +7,6 @@ export default class Scenery spawnPoints = []; bounds = { minX: Infinity, minZ: Infinity, maxX: -Infinity, maxZ: -Infinity }; electrificationResolved = ElectrificationResolutionStatus.NOT_RESOLVED; - static nextMiscId = 1; getBounds () { return { ...this.bounds };