Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
256 changes: 256 additions & 0 deletions internal/storage/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1278,6 +1278,262 @@ func TestSideIndexToString(t *testing.T) {
}
}

func TestParserV1_Parse_VehicleSparsePositions(t *testing.T) {
p := &ParserV1{}

// Simulate a static DShK that doesn't move for 10 frames, then moves.
// The Arma extension uses sparse format: each position entry has
// [startFrame, endFrame] at index 4, covering a range of frames.
data := map[string]interface{}{
"worldName": "Altis",
"missionName": "Test Sparse",
"endFrame": 15.0,
"captureDelay": 1.0,
"entities": []interface{}{
map[string]interface{}{
"id": 5.0,
"type": "vehicle",
"name": "DShK",
"class": "O_HMG_01_high_F",
"side": "EAST",
"startFrameNum": 0.0,
"positions": []interface{}{
// Static for frames 0-9: same position, no crew
[]interface{}{
[]interface{}{5000.0, 6000.0, 0.0}, // position
45.0, // direction
1.0, // alive
[]interface{}{}, // crew (empty)
[]interface{}{0.0, 9.0}, // [startFrame, endFrame] sparse range
},
// Moves for frames 10-14: different position, with crew
[]interface{}{
[]interface{}{5010.0, 6010.0, 0.0},
90.0,
1.0,
[]interface{}{3.0}, // crew member ID 3
[]interface{}{10.0, 14.0}, // frames 10-14
},
},
},
},
}

result, err := p.Parse(data, 300)
if err != nil {
t.Fatalf("Parse returned error: %v", err)
}

// Verify entity definition
if len(result.Entities) != 1 {
t.Fatalf("len(Entities) = %d, want 1", len(result.Entities))
}
ent := result.Entities[0]
if ent.EndFrame != 14 {
t.Errorf("Entity EndFrame = %d, want 14 (last sparse range end)", ent.EndFrame)
}

// Verify positions were expanded from 2 sparse entries to 15 dense entries
if len(result.EntityPositions) != 1 {
t.Fatalf("len(EntityPositions) = %d, want 1", len(result.EntityPositions))
}
ep := result.EntityPositions[0]

if len(ep.Positions) != 15 {
t.Fatalf("len(Positions) = %d, want 15 (frames 0-14 expanded from sparse)", len(ep.Positions))
}

// Verify frame 0 (first sparse range)
pos := ep.Positions[0]
if pos.FrameNum != 0 {
t.Errorf("Positions[0].FrameNum = %d, want 0", pos.FrameNum)
}
if pos.PosX != 5000.0 || pos.PosY != 6000.0 {
t.Errorf("Positions[0] pos = (%v, %v), want (5000, 6000)", pos.PosX, pos.PosY)
}
if pos.Direction != 45 {
t.Errorf("Positions[0].Direction = %d, want 45", pos.Direction)
}
if pos.Alive != 1 {
t.Errorf("Positions[0].Alive = %d, want 1", pos.Alive)
}

// Verify frame 5 (middle of first sparse range - should have same data)
pos = ep.Positions[5]
if pos.FrameNum != 5 {
t.Errorf("Positions[5].FrameNum = %d, want 5", pos.FrameNum)
}
if pos.PosX != 5000.0 || pos.PosY != 6000.0 {
t.Errorf("Positions[5] pos = (%v, %v), want (5000, 6000)", pos.PosX, pos.PosY)
}

// Verify frame 9 (end of first sparse range)
pos = ep.Positions[9]
if pos.FrameNum != 9 {
t.Errorf("Positions[9].FrameNum = %d, want 9", pos.FrameNum)
}
if pos.PosX != 5000.0 {
t.Errorf("Positions[9].PosX = %v, want 5000", pos.PosX)
}

// Verify frame 10 (start of second sparse range - different position)
pos = ep.Positions[10]
if pos.FrameNum != 10 {
t.Errorf("Positions[10].FrameNum = %d, want 10", pos.FrameNum)
}
if pos.PosX != 5010.0 || pos.PosY != 6010.0 {
t.Errorf("Positions[10] pos = (%v, %v), want (5010, 6010)", pos.PosX, pos.PosY)
}
if pos.Direction != 90 {
t.Errorf("Positions[10].Direction = %d, want 90", pos.Direction)
}
if len(pos.CrewIDs) != 1 || pos.CrewIDs[0] != 3 {
t.Errorf("Positions[10].CrewIDs = %v, want [3]", pos.CrewIDs)
}

// Verify frame 14 (end of second sparse range)
pos = ep.Positions[14]
if pos.FrameNum != 14 {
t.Errorf("Positions[14].FrameNum = %d, want 14", pos.FrameNum)
}
if pos.PosX != 5010.0 {
t.Errorf("Positions[14].PosX = %v, want 5010", pos.PosX)
}
}

func TestParserV1_Parse_VehicleSparsePositions_ChunkBuild(t *testing.T) {
// Verify that sparse vehicle positions produce correct chunk data
// (entity appears in every frame, not just first frame)
p := &ParserV1{}
w := &ProtobufWriterV1{}

data := map[string]interface{}{
"worldName": "Altis",
"missionName": "Test",
"endFrame": 10.0,
"captureDelay": 1.0,
"entities": []interface{}{
map[string]interface{}{
"id": 2.0,
"type": "vehicle",
"name": "Static Gun",
"class": "O_HMG_01_high_F",
"side": "EAST",
"startFrameNum": 0.0,
"positions": []interface{}{
[]interface{}{
[]interface{}{1000.0, 2000.0, 0.0},
180.0,
1.0,
[]interface{}{},
[]interface{}{0.0, 9.0}, // Covers all 10 frames
},
},
},
},
}

result, err := p.Parse(data, 300)
if err != nil {
t.Fatalf("Parse returned error: %v", err)
}

// Build a chunk and verify entity is present in EVERY frame
chunk := w.buildChunk(result, 0)
if len(chunk.Frames) != 10 {
t.Fatalf("len(Frames) = %d, want 10", len(chunk.Frames))
}

for i, frame := range chunk.Frames {
if len(frame.Entities) != 1 {
t.Errorf("Frame %d: len(Entities) = %d, want 1 (static vehicle should be present in every frame)", i, len(frame.Entities))
continue
}
ent := frame.Entities[0]
if ent.EntityId != 2 {
t.Errorf("Frame %d: EntityId = %d, want 2", i, ent.EntityId)
}
if ent.PosX != 1000.0 || ent.PosY != 2000.0 {
t.Errorf("Frame %d: pos = (%v, %v), want (1000, 2000)", i, ent.PosX, ent.PosY)
}
}
}

func TestParserV1_Parse_VehicleDensePositions_Unaffected(t *testing.T) {
// Verify that dense vehicle positions (without frame ranges) still work correctly
p := &ParserV1{}

data := map[string]interface{}{
"worldName": "Altis",
"missionName": "Test Dense",
"endFrame": 3.0,
"captureDelay": 1.0,
"entities": []interface{}{
map[string]interface{}{
"id": 1.0,
"type": "vehicle",
"name": "Truck",
"class": "B_Truck_01",
"startFrameNum": 0.0,
"positions": []interface{}{
// Dense format (no entry[4] frame range)
[]interface{}{[]interface{}{100.0, 200.0, 0.0}, 90.0, 1.0, []interface{}{}},
[]interface{}{[]interface{}{110.0, 210.0, 0.0}, 95.0, 1.0, []interface{}{0.0}},
[]interface{}{[]interface{}{120.0, 220.0, 0.0}, 100.0, 1.0, []interface{}{}},
},
},
},
}

result, err := p.Parse(data, 300)
if err != nil {
t.Fatalf("Parse returned error: %v", err)
}

ep := result.EntityPositions[0]
if len(ep.Positions) != 3 {
t.Fatalf("len(Positions) = %d, want 3 (dense, unchanged)", len(ep.Positions))
}

// Verify sequential frame numbers
for i, pos := range ep.Positions {
if pos.FrameNum != uint32(i) {
t.Errorf("Positions[%d].FrameNum = %d, want %d", i, pos.FrameNum, i)
}
}

// Verify end frame uses dense calculation
ent := result.Entities[0]
if ent.EndFrame != 2 { // 0 + 3 - 1
t.Errorf("EndFrame = %d, want 2", ent.EndFrame)
}
}

func TestParserV1_calculateEndFrame_SparseVehicle(t *testing.T) {
p := &ParserV1{}

t.Run("sparse vehicle positions", func(t *testing.T) {
em := map[string]interface{}{
"positions": []interface{}{
[]interface{}{
[]interface{}{100.0, 200.0, 0.0},
45.0, 1.0, []interface{}{},
[]interface{}{0.0, 499.0},
},
[]interface{}{
[]interface{}{150.0, 250.0, 0.0},
90.0, 1.0, []interface{}{},
[]interface{}{500.0, 999.0},
},
},
}
endFrame := p.calculateEndFrame(em, 0)
if endFrame != 999 {
t.Errorf("endFrame = %d, want 999 (from last sparse range)", endFrame)
}
})
}

func TestParserV1_collectEntityPositions_EdgeCases(t *testing.T) {
p := &ParserV1{}

Expand Down
47 changes: 39 additions & 8 deletions internal/storage/parser_v1.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,12 +112,22 @@ func (p *ParserV1) Parse(data map[string]interface{}, chunkSize uint32) (*ParseR
return result, nil
}

// calculateEndFrame determines the end frame from positions array length
// calculateEndFrame determines the end frame from positions array length.
// For sparse vehicle positions with [startFrame, endFrame] ranges, uses the last range's end.
func (p *ParserV1) calculateEndFrame(em map[string]interface{}, startFrame uint32) uint32 {
if positions, ok := em["positions"].([]interface{}); ok {
return startFrame + uint32(len(positions)) - 1
positions, ok := em["positions"].([]interface{})
if !ok || len(positions) == 0 {
return startFrame
}

// Check if the last position has a sparse frame range at index 4
if lastPos, ok := positions[len(positions)-1].([]interface{}); ok && len(lastPos) >= 5 {
if framesArr, ok := lastPos[4].([]interface{}); ok && len(framesArr) >= 2 {
return uint32(toFloat64(framesArr[1]))
}
}
return startFrame

return startFrame + uint32(len(positions)) - 1
}

// parseEvent converts a JSON event array to schema-agnostic Event
Expand Down Expand Up @@ -342,7 +352,9 @@ func (p *ParserV1) parseMarkerPosition(pos interface{}) *MarkerPosition {
return mp
}

// collectEntityPositions extracts position data for an entity
// collectEntityPositions extracts position data for an entity.
// For vehicles with sparse frame ranges ([startFrame, endFrame] at index 4),
// expands each sparse entry into one EntityPosition per frame in the range.
func (p *ParserV1) collectEntityPositions(em map[string]interface{}, entityID uint32, startFrame uint32, entityType string) *EntityPositionData {
positions, ok := em["positions"].([]interface{})
if !ok {
Expand All @@ -360,9 +372,7 @@ func (p *ParserV1) collectEntityPositions(em map[string]interface{}, entityID ui
continue
}

pos := EntityPosition{
FrameNum: startFrame + uint32(i),
}
pos := EntityPosition{}

// Parse position [x, y, z] or [x, y]
if coords, ok := posArr[0].([]interface{}); ok && len(coords) >= 2 {
Expand Down Expand Up @@ -410,6 +420,27 @@ func (p *ParserV1) collectEntityPositions(em map[string]interface{}, entityID ui
}
}

// Check for sparse frame range (vehicle format with [startFrame, endFrame] at index 4)
if entityType == "vehicle" && len(posArr) >= 5 {
if framesArr, ok := posArr[4].([]interface{}); ok && len(framesArr) >= 2 {
rangeStart := uint32(toFloat64(framesArr[0]))
rangeEnd := uint32(toFloat64(framesArr[1]))
// Expand: create one position entry per frame in the range
for f := rangeStart; f <= rangeEnd; f++ {
expanded := pos
expanded.FrameNum = f
if len(pos.CrewIDs) > 0 {
expanded.CrewIDs = make([]uint32, len(pos.CrewIDs))
copy(expanded.CrewIDs, pos.CrewIDs)
}
data.Positions = append(data.Positions, expanded)
}
continue
}
}

// Dense format: one position per frame
pos.FrameNum = startFrame + uint32(i)
data.Positions = append(data.Positions, pos)
}

Expand Down
Loading