Skip to content

Instantly share code, notes, and snippets.

@jrobinson3k1
Created January 8, 2025 17:26
Show Gist options
  • Select an option

  • Save jrobinson3k1/b4bcc6154402987dd46e9a9c3b30f30f to your computer and use it in GitHub Desktop.

Select an option

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,
)
}
@kesor
Copy link

kesor commented Jan 9, 2025

@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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment