-
-
Save jrobinson3k1/b4bcc6154402987dd46e9a9c3b30f30f to your computer and use it in GitHub Desktop.
| // parses a v3 PSG file | |
| object PassiveTreeParser { | |
| fun readData(inputStream: InputStream): SkillTreeGraph { | |
| val data = DataTypeReader(inputStream, order = ByteOrder.LITTLE_ENDIAN).readPassiveTreeData() | |
| val remainingBytes = inputStream.readAllBytes() | |
| require(remainingBytes.isEmpty()) { "$remainingBytes bytes were not processed" } | |
| return data | |
| } | |
| private fun DataTypeReader.readPassiveTreeData(): SkillTreeGraph = | |
| when (val version = readByte().toInt()) { | |
| 3 -> V3Decoder.decode(this) | |
| else -> throw UnsupportedOperationException("Missing decoder for version $version") | |
| } | |
| private object V3Decoder { | |
| fun decode(reader: DataTypeReader): SkillTreeGraph = with(reader) { | |
| val type = readByte() | |
| val orbitCount = readByte() | |
| val orbits = (0 until orbitCount).map { readByte().toUByte() } | |
| val rootNodeCount = readInt() | |
| val rootNodeIds = (0 until rootNodeCount).map { readInt().also { skip(4) } } | |
| val nodeGroupCount = readInt() | |
| val nodeGroups = (0 until nodeGroupCount).map { readNodeGroup() } | |
| return SkillTreeGraph( | |
| version = 3, | |
| type = type.toInt(), | |
| orbits = orbits.map { it.toInt() }, | |
| rootIds = rootNodeIds, | |
| groups = nodeGroups, | |
| ) | |
| } | |
| private fun DataTypeReader.readNodeGroup(): SkillTreeGraph.Group { | |
| val x = readFloat() | |
| val y = readFloat() | |
| val groupAssociationKey = readInt() | |
| val groupBackgroundOverride = readInt() | |
| val isJewelPositionReference = readByte().toInt() != 0 | |
| val nodeCount = readInt() | |
| val nodes = (0 until nodeCount).map { readNode() } | |
| return SkillTreeGraph.Group( | |
| location = x to y, | |
| groupAssociationKey = groupAssociationKey, | |
| groupBackgroundOverride = groupBackgroundOverride, | |
| isJewelPositionReference = isJewelPositionReference, | |
| nodes = nodes, | |
| ) | |
| } | |
| private fun DataTypeReader.readNode(): SkillTreeGraph.NodeRef { | |
| val id = readInt() | |
| val radius = readInt() | |
| val position = readInt() | |
| val connectionCount = readInt() | |
| val nodeConnections = (0 until connectionCount).map { readConnection() } | |
| return SkillTreeGraph.NodeRef( | |
| id = id, | |
| orbit = radius, | |
| orbitIndex = position, | |
| connectedNodeIds = nodeConnections, | |
| ) | |
| } | |
| private fun DataTypeReader.readConnection(): SkillTreeGraph.ConnectionRef { | |
| val nodeId = readInt() | |
| val orbit = readInt() | |
| return SkillTreeGraph.ConnectionRef( | |
| nodeId = nodeId, | |
| orbit = orbit, | |
| ) | |
| } | |
| } | |
| } |
| data class SkillTreeGraph( | |
| val version: Int, | |
| val type: Int, | |
| val orbits: List<Int>, | |
| val rootIds: List<Int>, | |
| val groups: List<Group>, | |
| ) { | |
| data class Group( | |
| val location: Pair<Float, Float>, | |
| val groupAssociationKey: Int, | |
| val groupBackgroundOverride: Int, | |
| val isJewelPositionReference: Boolean, | |
| val nodes: List<NodeRef>, | |
| ) | |
| data class NodeRef( | |
| val id: Int, | |
| val orbit: Int, | |
| val orbitIndex: Int, | |
| val connectedNodeIds: List<ConnectionRef>, | |
| ) | |
| data class ConnectionRef( | |
| val nodeId: Int, | |
| val orbit: Int, | |
| ) | |
| } |
@jrobinson3k1 I started digging into these binaries because I wanted to build a dependency graph, for calculation purposes. But I did end up cobbling a quick visualization for the x,y coords, and it does look a bit wonky, but maybe it is just the code that chatgpt wrote that is wrong idk.
Now that I have all the attribute data, names, icons, dependencies/relationships of the skills ... I can see the whole picture of how to make a script that will find "optimal" builds given different constraints. My worry was that hard-coding all the data manually would quickly become out-of-sync with the actual game, which is why I wanted to read it directly from the game files instead.
And I already had the datc64 decoded, there are several places that have the "schema" for each of the files available. Noted the graph_id column in that database, which is why I wanted to read the psg.
Also,
orbitinsideConnectionneeds to be signed. The rest I think are safe to be unsigned except thexandyfloats. The sign on the orbit value inside the connection determines which of the two intersection circles to use when drawing connecting arcs between nodes. A value of zero mean that an arc should be drawn along the shared orbit of two nodes who share the same group and orbit. For all other connections with an orbit value of zero, a straight line should be drawn between them.