Skip to content

Commit

Permalink
Add node graph extraction
Browse files Browse the repository at this point in the history
  • Loading branch information
L-P committed Oct 7, 2024
1 parent f6acda3 commit 0918f57
Show file tree
Hide file tree
Showing 5 changed files with 339 additions and 0 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# v1.2.0
- Add graph node extraction.

# v1.1.1
- Replace help command with a single manpage.

Expand Down
188 changes: 188 additions & 0 deletions goldsrc/node.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
package goldsrc

import (
"encoding/binary"
"fmt"
"io"
)

// Original retail version.
const NodeGraphVersion = 16

type NodeFormat int

const (
NodeFormatValve NodeFormat = iota
NodeFormatDecay
)

type Graph struct {
_ [3]int32 // 3 qbool
_ [3]uint32 // 3 pointers

NumNodes int32
NumLinks int32

_ [8364]byte
}

type Vec3f struct {
X, Y, Z float32
}

func (vec Vec3f) String() string { // .map-compatible
return fmt.Sprintf("%f %f %f", vec.X, vec.Y, vec.Z)
}

type Node interface {
Position(original bool) Vec3f // false to get position after being dropped.
ClassName() string
}

type ValveNode struct {
Origin Vec3f
OriginPeek Vec3f

_ [3]byte

NodeInfo int32

_ [57]byte
}

type DecayNode struct {
ValveNode
_ [8]byte
}

func (node ValveNode) Position(original bool) Vec3f {
if original {
return node.Origin
}

return node.OriginPeek
}

const (
NodeTypeLand int32 = 1 << 0
NodeTypeAir int32 = 1 << 1
NodeTypeWater int32 = 1 << 2
)

func (node ValveNode) ClassName() string {
// Order matters.
switch {
case node.NodeInfo == 256:
// HACK: No idea where this 256 comes from.
return "info_node"
case (node.NodeInfo & NodeTypeLand) != 0, node.NodeInfo == 0:
return "info_node"
case (node.NodeInfo & NodeTypeWater) != 0:
return "info_node_water"
case (node.NodeInfo & NodeTypeAir) != 0:
return "info_node_air"
default:
return fmt.Sprintf("info_node_unknown_%d", node.NodeInfo)
}
}

const (
LinkTypeSmallHull = iota
LinkTypeHumanHull
LinkTypeLargeHull
LinkTypeFlyHull
LinkTypeDisabledHull

LinkTypeBitMax = 4
)

func LinkTypeName(id int) string {
switch id {
case LinkTypeSmallHull:
return "small"
case LinkTypeHumanHull:
return "human"
case LinkTypeLargeHull:
return "large"
case LinkTypeFlyHull:
return "fly"
case LinkTypeDisabledHull:
return "disabled"
default:
return "unknown"
}
}

type Link struct {
SrcNode int32
DstNode int32
_ [8]byte
LinkInfo int32
_ [4]byte
}

func ReadNodes(r io.Reader, format NodeFormat) ([]Node, []Link, error) {
var version int32
if err := binary.Read(r, binary.LittleEndian, &version); err != nil {
return nil, nil, fmt.Errorf("unable to parse version: %w", err)
}
if version != NodeGraphVersion {
return nil, nil, fmt.Errorf("unsupported node graph version: %d", version)
}

var graph Graph
if err := binary.Read(r, binary.LittleEndian, &graph); err != nil {
return nil, nil, fmt.Errorf("unable to read CGraph: %w", err)
}

var nodes = make([]Node, 0, graph.NumNodes)
for i := int32(0); i < graph.NumNodes; i++ {
node, err := readNode(r, format)
if err != nil {
return nil, nil, fmt.Errorf("unable to read node #%d: %w", i, err)
}

nodes = append(nodes, node)
}

var links = make([]Link, 0, graph.NumLinks)
for i := int32(0); i < graph.NumLinks; i++ {
var link Link
if err := binary.Read(r, binary.LittleEndian, &link); err != nil {
return nil, nil, fmt.Errorf("unable to read CLink: %w", err)
}

links = append(links, link)
}

return nodes, links, nil
}

func readNode(r io.Reader, format NodeFormat) (Node, error) {
switch format {
case NodeFormatValve:
var node ValveNode
var err = binary.Read(r, binary.LittleEndian, &node)
return node, err
case NodeFormatDecay:
var node DecayNode
var err = binary.Read(r, binary.LittleEndian, &node)
return node, err
default:
return nil, fmt.Errorf("unknown node format: %d", format)
}
}

func assertSizeof(typ any, expected int) {
var actual = binary.Size(typ)
if actual != expected {
panic(fmt.Errorf("invalid size for %T: got %d expected %d", typ, actual, expected))
}
}

func init() {
assertSizeof(Graph{}, 8396)
assertSizeof(ValveNode{}, 88)
assertSizeof(DecayNode{}, 96)
assertSizeof(Link{}, 24)
}
36 changes: 36 additions & 0 deletions goldutil.1
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ map export|graph
.I FILE
.br
.B goldutil
nod export
.I FILE
.br
.B goldutil
spr create|extract|info
.I FILE
.br
Expand Down Expand Up @@ -88,6 +92,38 @@ written to stdout.
.RE
.\" }}}
.\" {{{
.SH NOD MANIPULATION
.B goldutil
nod export
[-\-input-format \fIFORMAT\fR]
.I FILE
.RS 4
Extract node positions from a .nod graph into a .map populated with
corresponding info_node entities.
.br
Links between nodes are represented using target/targetname, nodes are
duplicated to allow showing all links, TrenchBroom layers are used to separate
links by hull type. The resulting .map file is not for engine consumption, only
for TrenchBroom-assisted archaeology.
.IP "-\-input-format \fIFORMAT\fR" 4
Parse the .nod file using a different node graph format instead of using the
PC release format.
\fIFORMAT\fR can be any one of:
.RS 8
\(bu
\fIvalve\fR\ Standard Half-Life node graph (default).
.br
\(bu
\fIdecay\fR\ PlayStation 2 release of Half-Life: Decay.
.RE
.IP -\-original-positions 4
Use the node positions as they were set in the original .map instead of their
position after being dropped to the ground during graph generation.
.RE
.\"
.PP
.\" }}}
.\" {{{
.SH SPR MANIPULATION
.B goldutil
spr create
Expand Down
82 changes: 82 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"path/filepath"
"runtime"
"sort"
"strconv"
"strings"

"github.com/urfave/cli/v2"
Expand Down Expand Up @@ -45,6 +46,24 @@ func newApp() *cli.App {
Name: "help",
Action: doHelp,
},
{
Name: "nod",
Subcommands: []*cli.Command{
{
Name: "export",
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "original-positions",
},
&cli.StringFlag{
Name: "input-format",
Value: "valve",
},
},
Action: doNodExport,
},
},
},
{
Name: "map",
Subcommands: []*cli.Command{
Expand Down Expand Up @@ -466,3 +485,66 @@ func doHelp(cCtx *cli.Context) error {

return nil
}

func doNodExport(cCtx *cli.Context) error {
format, ok := map[string]goldsrc.NodeFormat{
"valve": goldsrc.NodeFormatValve,
"decay": goldsrc.NodeFormatDecay,
}[cCtx.String("input-format")]
if !ok {
return errors.New("unrecognize .nod format")
}

f, err := os.Open(cCtx.Args().Get(0))
if err != nil {
return fmt.Errorf("unable to open file for reading: %w", err)
}
defer f.Close()

nodes, links, err := goldsrc.ReadNodes(f, format)
if err != nil {
return fmt.Errorf("unable to read nodes: %w", err)
}

var (
original = cCtx.Bool("original-positions")
entities = make([]qmap.Entity, 0, len(nodes)+len(links))
)

for i, v := range nodes {
entities = append(entities, qmap.NewEntity(map[string]string{
qmap.KClass: v.ClassName(),
qmap.KOrigin: v.Position(original).String(),
qmap.KName: fmt.Sprintf("node#%d", i),
}))
}

for linkTypeBitID := 0; linkTypeBitID <= goldsrc.LinkTypeBitMax; linkTypeBitID++ {
entities = append(entities, qmap.NewEntity(map[string]string{
qmap.KClass: "func_group",
"_tb_type": "_tb_layer",
"_tb_name": fmt.Sprintf("hull#%d links (%s)", linkTypeBitID, goldsrc.LinkTypeName(linkTypeBitID)),
"_tb_id": strconv.Itoa(linkTypeBitID + 1),
"_tb_layer_sort_index": strconv.Itoa(linkTypeBitID + 1),
}))

for _, v := range links {
if (v.LinkInfo & (1 << linkTypeBitID)) == 0 {
continue
}

src := entities[v.SrcNode]
src.SetProperty("target", fmt.Sprintf("node#%d", v.DstNode))
src.SetProperty("_tb_layer", strconv.Itoa(linkTypeBitID+1))
entities = append(entities, src)
}
}

var out qmap.QMap
for _, v := range entities {
out.AddEntity(v)
}
fmt.Println(out.String())

return nil
}
30 changes: 30 additions & 0 deletions qmap/entity.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,20 @@ type Entity struct {
keyLookup map[string]string
}

func NewEntity(props map[string]string) Entity {
var ent Entity
for k, v := range props {
ent.addProperty(Property{
key: k,
value: v,
})
}

ent.finalize()

return ent
}

type Property struct {
line int
key, value string
Expand Down Expand Up @@ -83,6 +97,22 @@ func (e *Entity) addProperty(p Property) {
e.props = append(e.props, p)
}

func (e *Entity) SetProperty(k, v string) {
e.keyLookup[k] = v

for i := range e.props {
if e.props[i].key == k {
e.props[i].value = v
return
}
}

e.addProperty(Property{
key: k,
value: v,
})
}

func (e *Entity) RemoveProperty(key string) {
var filtered = make([]Property, 0, len(e.props))
for _, v := range e.props {
Expand Down

0 comments on commit 0918f57

Please sign in to comment.