Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
8865e48
Added schedule
D3rPole Apr 23, 2024
30d9f9e
Added arrow pointing to current target on schedule
D3rPole Apr 23, 2024
b833cac
added reminder cuz i forget easily :)
D3rPole Apr 23, 2024
d6d52f2
added currentPath of train to api/trains (Autopilot path)
D3rPole Apr 24, 2024
c334970
Paths now somehow get visualised, turns out the data is only about th…
D3rPole Apr 24, 2024
c410242
Path turns are now curves on the network
D3rPole Apr 24, 2024
f4446dd
Most of the Path is displayed now
D3rPole Apr 26, 2024
64b6495
Train to first node connected!
D3rPole Apr 26, 2024
abc9434
Current train path now gets completely displayed.
D3rPole Apr 26, 2024
1f824bb
Removed debug path from api/train and all its usages
D3rPole Apr 26, 2024
bdaccf1
Portal support for current path
D3rPole Apr 27, 2024
de29b24
Added tripDistance and distanceToDrive to api/trains -> currentPath
D3rPole Apr 27, 2024
b2c382b
Trains don't get redrawn every train update, they get moved instead. …
D3rPole Apr 28, 2024
a0ef283
Train paths will now be stored in "trainPaths" layer which can be tur…
D3rPole Apr 28, 2024
4d50170
Added train info, window opens when train is clicked. Used leaflet-co…
D3rPole Apr 29, 2024
ae98b6b
Train path is only displayed when the same trains train info window i…
D3rPole Apr 29, 2024
1c6c130
Schedule times are now displayed in train info window (not accounting…
D3rPole Apr 30, 2024
69c1765
added enable_navigation_tracks to config
D3rPole Apr 30, 2024
06513a0
cleaned up a little
D3rPole Apr 30, 2024
3bd7447
Clicking on a train in trainlist now opens the train info window.
D3rPole May 3, 2024
ee72243
Refactor and docs
D3rPole May 5, 2024
98db912
added lazy fix to clicking a train when it has no leading data for th…
D3rPole May 20, 2024
9226566
Fixed faulty path when scheduled train drives backwards
D3rPole Jun 16, 2024
3d2b462
Fixed path displaying wrong when backwards driving train stopped (at …
D3rPole Jun 17, 2024
1b51c95
Fixed path retrieval using wrong end of the train as a start point wh…
D3rPole Jun 17, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions LICENSE_leaflet_control_window.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
The MIT License (MIT)

Copyright (c) 2015, Filip Zavadil
All rights reserved.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
208 changes: 199 additions & 9 deletions src/main/kotlin/littlechasiu/ctm/Extensions.kt
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
package littlechasiu.ctm

import com.simibubi.create.content.trains.entity.Carriage
import com.simibubi.create.content.trains.entity.Navigation
import com.simibubi.create.content.trains.entity.Train
import com.simibubi.create.content.trains.entity.TravellingPoint
import com.simibubi.create.content.trains.graph.TrackEdge
import com.simibubi.create.content.trains.graph.TrackGraph
import com.simibubi.create.content.trains.graph.TrackNode
import com.simibubi.create.content.trains.graph.TrackNodeLocation
import com.simibubi.create.content.trains.schedule.ScheduleRuntime
import com.simibubi.create.content.trains.schedule.destination.ChangeThrottleInstruction
import com.simibubi.create.content.trains.schedule.destination.ChangeTitleInstruction
import com.simibubi.create.content.trains.schedule.destination.DestinationInstruction
import com.simibubi.create.content.trains.station.GlobalStation
import com.simibubi.create.foundation.utility.Couple
import littlechasiu.ctm.model.*
import net.minecraft.resources.ResourceKey
Expand Down Expand Up @@ -44,6 +51,7 @@ val TrackNode.dimensionLocation: DimensionLocation
get() =
DimensionLocation(location.dimension.string, location.sendable)

@Suppress("IMPLICIT_CAST_TO_ANY")
val TrackEdge.sendable
get() =
if (isInterDimensional)
Expand Down Expand Up @@ -75,13 +83,195 @@ val Carriage.sendable
},
)

val Train.sendable
get() =
CreateTrain(
id = id,
name = name.string,
owner = null,
cars = carriages.map { it.sendable }.toList(),
backwards = speed < 0,
stopped = speed == 0.0,
val ScheduleRuntime.sendable
get() = schedule?.let {
val field = ScheduleRuntime::class.java.getDeclaredField("ticksInTransit")
field.isAccessible = true
val currentTime = field.get(this) as Int


CreateSchedule(
cycling = it.cyclic,
instructions = getInstructions(this),
paused = paused,
currentEntry = currentEntry,
ticksInTransit = currentTime,
)
}

/**
* gets schedule instructions for a train
* @param scheduleRuntime ScheduleRuntime object associated with the Train
* @return ArrayList<ScheduleInstruction> result
*/
fun getInstructions(scheduleRuntime: ScheduleRuntime): ArrayList<ScheduleInstruction> {
val result: ArrayList<ScheduleInstruction> = ArrayList()
val instructions = scheduleRuntime.schedule.entries

val field = ScheduleRuntime::class.java.getDeclaredField("predictionTicks")
field.isAccessible = true
@Suppress("UNCHECKED_CAST")
val predictionTicks = field.get(scheduleRuntime) as List<Int>

instructions.forEachIndexed { i, entry ->
when (val instruction = entry.instruction) {
is DestinationInstruction -> {
val stationName = instruction.summary.second.string
result.add(ScheduleInstructionDestination(stationName = stationName, ticksToComplete = predictionTicks[i]))
}
is ChangeTitleInstruction -> {
val newName = instruction.scheduleTitle
result.add(ScheduleInstructionNameChange(newName = newName))
}
is ChangeThrottleInstruction -> {
val throttle = instruction.summary.second.string
result.add(ScheduleInstructionThrottleChange(throttle = throttle))
}
}
}
return result
}

val Train.sendable: CreateTrain
get() {
return CreateTrain(
id = id,
name = name.string,
owner = null,
cars = carriages.map { it.sendable }.toList(),
speed = speed,
backwards = speed < 0,
stopped = speed == 0.0,
schedule = runtime.sendable,
currentPath = getCurrentTrainPath(navigation),
)
}

/*Path calculation methods*/

/**
* Calculates next TrackEdge when the train isn't turning.
* @param graph TrackGraph the train is currently on.
* @param trackEdge TrackEdge the train is currently on.
* @param trackNode TrackNode the train is driving towards
* @param direction Vec3 direction where the track / train is pointing
* @return TrackEdge result
*/
private fun getNextEdge(graph: TrackGraph, trackEdge: TrackEdge, trackNode: TrackNode, direction: Vec3) : TrackEdge?{
var result : TrackEdge? = null
var biggest = Double.MIN_VALUE
graph.getConnectionsFrom(trackNode).forEach { key, value ->
if (key == trackEdge) {
return@forEach
}
val dot = value.getDirection(true).dot(direction)
if(dot > biggest && dot > 0){
biggest = dot
result = value
}
}
return result
}

/**
* Calculates path when the train keeps going without turning.
* This is not a pathfinder endNode must be reachable with no turns.
* Method stops after 100 iterations by itself
* @param startEdge TrackEdge start point
* @param endNode TrackEdge end point
* @param graph TrackGraph the train is currently on
* @return ArrayList<Edge> result
*/
private fun straightLinePathToEndNode(startEdge: TrackEdge, endNode: TrackNode, graph: TrackGraph) : ArrayList<Edge> {
val result = ArrayList<Edge>()

var tEdge: TrackEdge = startEdge

var direction: Vec3
var reachedEnd = false
val MAX_PREDICTIONS = 100

var j = 0
while (!reachedEnd) {
direction = tEdge.getDirection(false)
tEdge = getNextEdge(graph, tEdge, tEdge.node2, direction) ?: return result

j++
if (tEdge.node1.netId == endNode.netId) {
reachedEnd = true // incase tEdge is the next scheduled edge
} else {
val edge = tEdge.sendable
if(edge is Edge) { // incase edge is portal, then ignore it
result.add(edge)
}
}

if (tEdge.node2.netId == endNode.netId || j > MAX_PREDICTIONS) {
reachedEnd = true
}
}
return result
}

/**
* Gets the TrackEdge a train station
* @param station GlobalStation train station
* @param graph TrackGraph the train station is on
* @return TrackEdge result
*/
private fun getEdgeFromStation(station: GlobalStation, graph: TrackGraph) : TrackEdge {
val firstNode = graph.locateNode(station.edgeLocation.first)
val secondNode = graph.locateNode(station.edgeLocation.second)
return graph.getConnection(Couple.create(firstNode, secondNode))
}

/**
* Calculates the current path the train is taking
* @param navigation Navigation object of Train
* @return Path information object
*/
private fun getCurrentTrainPath(navigation: Navigation?) : Path{
val graph = navigation?.train?.graph
val result : ArrayList<Edge> = ArrayList()
if(!CreateTrain.enableNavigationTracks || navigation == null || graph == null || navigation.destination == null){
return Path(result,0.0,0.0)
}
val field = Navigation::class.java.getDeclaredField("currentPath")
field.isAccessible = true
@Suppress("UNCHECKED_CAST")
val currentPath = field.get(navigation) as List<Couple<TrackNode>>

val endPointEdge = if(navigation.train.currentlyBackwards) navigation.train.endpointEdges.second else navigation.train.endpointEdges.first
val firstNode = endPointEdge.first
val secondNode = endPointEdge.second
val firstEdge: TrackEdge = if(navigation.train.currentlyBackwards)
graph.getConnection(Couple.create(secondNode, firstNode)) // get the "inverted" edge when driving backwards
else
graph.getConnection(Couple.create(firstNode, secondNode))

val lastEdge: TrackEdge = getEdgeFromStation(navigation.destination, graph)
if(firstEdge == lastEdge){
return Path(result,0.0,0.0)
}

result.add(firstEdge.sendable as Edge)
result.add(lastEdge.sendable as Edge)
if(currentPath.isNotEmpty()){
result.addAll(straightLinePathToEndNode(firstEdge, currentPath[0].first, graph))
result.addAll(straightLinePathToEndNode(graph.getConnection(currentPath[currentPath.size - 1]), lastEdge.node1, graph))
}else{
result.addAll(straightLinePathToEndNode(firstEdge, lastEdge.node1, graph))
}

currentPath.forEachIndexed{i, obj ->
val trackEdge : TrackEdge = graph.getConnectionsFrom(obj.first).get(obj.second) ?: return@forEachIndexed
val edge = trackEdge.sendable
if(edge !is Edge) {return@forEachIndexed}
result.add(edge)

if(i < currentPath.size - 1){
result.addAll(straightLinePathToEndNode(trackEdge, currentPath[i + 1].first, graph))
}
}
return Path(result,navigation.distanceStartedAt,navigation.distanceToDestination)
}
1 change: 1 addition & 0 deletions src/main/kotlin/littlechasiu/ctm/Server.kt
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ class Server {
"track-occupied" to Color(mapStyle.colors.track.occupied),
"track-reserved" to Color(mapStyle.colors.track.reserved),
"track-free" to Color(mapStyle.colors.track.free),
"track-path" to Color(mapStyle.colors.track.path),
"signal-green" to Color(mapStyle.colors.signal.green),
"signal-yellow" to Color(mapStyle.colors.signal.yellow),
"signal-red" to Color(mapStyle.colors.signal.red),
Expand Down
2 changes: 2 additions & 0 deletions src/main/kotlin/littlechasiu/ctm/TrackMap.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream
import kotlinx.serialization.json.encodeToStream
import littlechasiu.ctm.model.Config
import littlechasiu.ctm.model.CreateTrain
import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents
import net.fabricmc.loader.api.FabricLoader
Expand Down Expand Up @@ -77,6 +78,7 @@ object TrackMap {
watcher.enable = config.enable
server.enable = config.enable
watcher.watchInterval = config.watchIntervalSeconds.seconds
CreateTrain.enableNavigationTracks = config.enableNavigationTracks
server.port = config.serverPort
server.mapStyle = config.mapStyle
server.mapView = config.mapView
Expand Down
7 changes: 7 additions & 0 deletions src/main/kotlin/littlechasiu/ctm/model/Config.kt
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ data class TrackColors(
val reserved: String = "pink",
@EncodeDefault
val free: String = "white",
@EncodeDefault
val path: String = "#2244FF",
)

@Serializable
Expand Down Expand Up @@ -140,6 +142,10 @@ data class Config @OptIn(ExperimentalSerializationApi::class) constructor(
@SerialName("server_port")
@EncodeDefault
val serverPort: Int = 3876,
@SerialName("enable_navigation_tracks")
@EncodeDefault
val enableNavigationTracks: Boolean = true,


@SerialName("map_style")
@EncodeDefault
Expand All @@ -161,5 +167,6 @@ data class Config @OptIn(ExperimentalSerializationApi::class) constructor(
"portals" to LayerConfig(label = "Portals"),
"stations" to LayerConfig(label = "Stations"),
"trains" to LayerConfig(label = "Trains"),
"trainPaths" to LayerConfig(label = "Train navigation"),
),
)
53 changes: 44 additions & 9 deletions src/main/kotlin/littlechasiu/ctm/model/Network.kt
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,6 @@ data class Point(
val z: Double,
)

@Serializable
data class Path(
val start: Point,
val firstControlPoint: Point,
val secondControlPoint: Point,
val end: Point,
)

@Serializable
data class Edge(
val dimension: String,
Expand Down Expand Up @@ -120,6 +112,42 @@ data class TrainCar(
val portal: Portal? = null,
)

@Serializable
sealed class ScheduleInstruction(
val instructionType: String,
)
@Serializable
data class ScheduleInstructionDestination(
val stationName : String,
val ticksToComplete : Int,
) : ScheduleInstruction(instructionType = "Destination")

@Serializable
data class ScheduleInstructionThrottleChange(
val throttle : String,
) : ScheduleInstruction(instructionType = "ThrottleChange")

@Serializable
data class ScheduleInstructionNameChange(
val newName : String,
) : ScheduleInstruction(instructionType = "NameChange")

@Serializable
data class CreateSchedule(
val instructions: List<ScheduleInstruction>,
val cycling: Boolean,
val paused: Boolean,
val currentEntry: Int,
val ticksInTransit: Int,
)

@Serializable
data class Path(
val path : List<Edge>,
val tripDistance : Double,
val distanceToDrive : Double
)

@Serializable
data class CreateTrain(
@Serializable(with = UUIDSerializer::class)
Expand All @@ -129,7 +157,14 @@ data class CreateTrain(
val cars: List<TrainCar>,
val backwards: Boolean,
val stopped: Boolean,
)
val speed: Double,
val schedule: CreateSchedule?,
val currentPath: Path,
){
companion object {
var enableNavigationTracks: Boolean = true
}
}

@Serializable
data class TrainStatus(
Expand Down
Loading