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
19 changes: 16 additions & 3 deletions cmd/ocap_recorder/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import (
"github.com/OCAP2/extension/v5/internal/handlers"
"github.com/OCAP2/extension/v5/internal/logging"
"github.com/OCAP2/extension/v5/internal/model"
"github.com/OCAP2/extension/v5/internal/model/convert"
"github.com/OCAP2/extension/v5/internal/monitor"
intOtel "github.com/OCAP2/extension/v5/internal/otel"
"github.com/OCAP2/extension/v5/internal/storage"
Expand Down Expand Up @@ -863,10 +864,22 @@ func registerLifecycleHandlers(d *dispatcher.Dispatcher) {

// Time state tracking - records mission time sync data
d.Register(":NEW:TIME:STATE:", func(e dispatcher.Event) (any, error) {
// Time state is currently not stored, just acknowledged
// Args: [frameNo, systemTimeUTC, missionDateTime, timeMultiplier, missionTime]
if handlerService == nil {
return nil, nil
}

obj, err := handlerService.LogTimeState(e.Args)
if err != nil {
return nil, fmt.Errorf("failed to log time state: %w", err)
}

if storageBackend != nil {
coreObj := convert.TimeStateToCore(obj)
storageBackend.RecordTimeState(&coreObj)
}

return "ok", nil
})
}, dispatcher.Buffered(100))

d.Register(":SAVE:MISSION:", func(e dispatcher.Event) (any, error) {
Logger.Info("Received :SAVE:MISSION: command, ending mission recording")
Expand Down
44 changes: 44 additions & 0 deletions internal/handlers/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -1215,6 +1215,50 @@ func (s *Service) LogFpsEvent(data []string) (model.ServerFpsEvent, error) {
return fpsEvent, nil
}

// LogTimeState parses time state data and returns a TimeState model
// Args: [frameNo, systemTimeUTC, missionDateTime, timeMultiplier, missionTime]
func (s *Service) LogTimeState(data []string) (model.TimeState, error) {
var timeState model.TimeState

// fix received data
for i, v := range data {
data[i] = util.FixEscapeQuotes(util.TrimQuotes(v))
}

// get frame
frameStr := data[0]
capframe, err := strconv.ParseFloat(frameStr, 64)
if err != nil {
return timeState, fmt.Errorf(`error converting capture frame to int: %s`, err)
}

timeState.CaptureFrame = uint(capframe)
timeState.Time = time.Now()
timeState.Mission = *s.ctx.GetMission()

// systemTimeUTC - e.g., "2024-01-15T14:30:45.123"
timeState.SystemTimeUTC = data[1]

// missionDateTime - e.g., "2035-06-15T06:00:00"
timeState.MissionDate = data[2]

// timeMultiplier
timeMult, err := strconv.ParseFloat(data[3], 64)
if err != nil {
return timeState, fmt.Errorf(`error converting timeMultiplier to float: %v`, err)
}
timeState.TimeMultiplier = float32(timeMult)

// missionTime (seconds since start)
missionTime, err := strconv.ParseFloat(data[4], 64)
if err != nil {
return timeState, fmt.Errorf(`error converting missionTime to float: %v`, err)
}
timeState.MissionTime = float32(missionTime)

return timeState, nil
}

// LogAce3DeathEvent parses ACE3 death event data and returns an Ace3DeathEvent model
func (s *Service) LogAce3DeathEvent(data []string) (model.Ace3DeathEvent, error) {
var deathEvent model.Ace3DeathEvent
Expand Down
1 change: 1 addition & 0 deletions internal/handlers/handlers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ func (b *mockBackend) RecordKillEvent(e *core.KillEvent) error { ret
func (b *mockBackend) RecordChatEvent(e *core.ChatEvent) error { return nil }
func (b *mockBackend) RecordRadioEvent(e *core.RadioEvent) error { return nil }
func (b *mockBackend) RecordServerFpsEvent(e *core.ServerFpsEvent) error { return nil }
func (b *mockBackend) RecordTimeState(t *core.TimeState) error { return nil }
func (b *mockBackend) RecordAce3DeathEvent(e *core.Ace3DeathEvent) error { return nil }
func (b *mockBackend) RecordAce3UnconsciousEvent(e *core.Ace3UnconsciousEvent) error {
return nil
Expand Down
13 changes: 13 additions & 0 deletions internal/model/convert/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,19 @@ func ServerFpsEventToCore(e model.ServerFpsEvent) core.ServerFpsEvent {
}
}

// TimeStateToCore converts a GORM TimeState to a core.TimeState
func TimeStateToCore(t model.TimeState) core.TimeState {
return core.TimeState{
MissionID: t.MissionID,
Time: t.Time,
CaptureFrame: t.CaptureFrame,
SystemTimeUTC: t.SystemTimeUTC,
MissionDate: t.MissionDate,
TimeMultiplier: t.TimeMultiplier,
MissionTime: t.MissionTime,
}
}

// Ace3DeathEventToCore converts a GORM Ace3DeathEvent to a core.Ace3DeathEvent
func Ace3DeathEventToCore(e model.Ace3DeathEvent) core.Ace3DeathEvent {
result := core.Ace3DeathEvent{
Expand Down
36 changes: 36 additions & 0 deletions internal/model/convert/convert_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,41 @@ func TestServerFpsEventToCore(t *testing.T) {
}
}

func TestTimeStateToCore(t *testing.T) {
now := time.Now()

gormState := model.TimeState{
MissionID: 1,
Time: now,
CaptureFrame: 100,
SystemTimeUTC: "2024-01-15T14:30:45.123",
MissionDate: "2035-06-15T06:00:00",
TimeMultiplier: 2.0,
MissionTime: 3600.5,
}

coreState := TimeStateToCore(gormState)

if coreState.MissionID != 1 {
t.Errorf("expected MissionID=1, got %d", coreState.MissionID)
}
if coreState.CaptureFrame != 100 {
t.Errorf("expected CaptureFrame=100, got %d", coreState.CaptureFrame)
}
if coreState.SystemTimeUTC != "2024-01-15T14:30:45.123" {
t.Errorf("expected SystemTimeUTC=2024-01-15T14:30:45.123, got %s", coreState.SystemTimeUTC)
}
if coreState.MissionDate != "2035-06-15T06:00:00" {
t.Errorf("expected MissionDate=2035-06-15T06:00:00, got %s", coreState.MissionDate)
}
if coreState.TimeMultiplier != 2.0 {
t.Errorf("expected TimeMultiplier=2.0, got %f", coreState.TimeMultiplier)
}
if coreState.MissionTime != 3600.5 {
t.Errorf("expected MissionTime=3600.5, got %f", coreState.MissionTime)
}
}

func TestAce3DeathEventToCore(t *testing.T) {
now := time.Now()

Expand Down Expand Up @@ -606,6 +641,7 @@ var (
_ core.ChatEvent = ChatEventToCore(model.ChatEvent{})
_ core.RadioEvent = RadioEventToCore(model.RadioEvent{})
_ core.ServerFpsEvent = ServerFpsEventToCore(model.ServerFpsEvent{})
_ core.TimeState = TimeStateToCore(model.TimeState{})
_ core.Ace3DeathEvent = Ace3DeathEventToCore(model.Ace3DeathEvent{})
_ core.Ace3UnconsciousEvent = Ace3UnconsciousEventToCore(model.Ace3UnconsciousEvent{})
_ core.Marker = MarkerToCore(model.Marker{})
Expand Down
12 changes: 12 additions & 0 deletions internal/model/core/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,3 +116,15 @@ type Ace3UnconsciousEvent struct {
CaptureFrame uint
IsAwake bool
}

// TimeState represents mission time synchronization data
type TimeState struct {
ID uint
MissionID uint
Time time.Time
CaptureFrame uint
SystemTimeUTC string
MissionDate string
TimeMultiplier float32
MissionTime float32
}
16 changes: 16 additions & 0 deletions internal/model/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -677,6 +677,22 @@ func (s *ServerFpsEvent) TableName() string {
return "server_fps_events"
}

// TimeState represents mission time synchronization data
type TimeState struct {
Time time.Time `json:"time" gorm:"type:timestamptz;"`
MissionID uint `json:"missionId" gorm:"index:idx_timestate_mission_id"`
Mission Mission `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;foreignkey:MissionID;"`
CaptureFrame uint `json:"captureFrame" gorm:"index:idx_timestate_capture_frame;"`
SystemTimeUTC string `json:"systemTimeUtc" gorm:"size:64"`
MissionDate string `json:"missionDate" gorm:"size:64"`
TimeMultiplier float32 `json:"timeMultiplier"`
MissionTime float32 `json:"missionTime"`
}

func (t *TimeState) TableName() string {
return "time_states"
}

// Marker represents a map marker
type Marker struct {
ID uint `json:"id" gorm:"primarykey;autoIncrement;"`
Expand Down
10 changes: 10 additions & 0 deletions internal/storage/memory/memory.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ type Backend struct {
chatEvents []core.ChatEvent
radioEvents []core.RadioEvent
serverFpsEvents []core.ServerFpsEvent
timeStates []core.TimeState
ace3DeathEvents []core.Ace3DeathEvent
ace3UnconsciousEvents []core.Ace3UnconsciousEvent

Expand Down Expand Up @@ -91,6 +92,7 @@ func (b *Backend) StartMission(mission *core.Mission, world *core.World) error {
b.chatEvents = nil
b.radioEvents = nil
b.serverFpsEvents = nil
b.timeStates = nil
b.ace3DeathEvents = nil
b.ace3UnconsciousEvents = nil
b.idCounter = 0
Expand Down Expand Up @@ -290,6 +292,14 @@ func (b *Backend) RecordServerFpsEvent(e *core.ServerFpsEvent) error {
return nil
}

// RecordTimeState records a time synchronization state
func (b *Backend) RecordTimeState(t *core.TimeState) error {
b.mu.Lock()
defer b.mu.Unlock()
b.timeStates = append(b.timeStates, *t)
return nil
}

// RecordAce3DeathEvent records an ACE3 death event
func (b *Backend) RecordAce3DeathEvent(e *core.Ace3DeathEvent) error {
b.mu.Lock()
Expand Down
33 changes: 33 additions & 0 deletions internal/storage/memory/memory_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -549,6 +549,35 @@ func TestRecordServerFpsEvent(t *testing.T) {
}
}

func TestRecordTimeState(t *testing.T) {
b := New(config.MemoryConfig{})

now := time.Now()
state := &core.TimeState{
MissionID: 1,
Time: now,
CaptureFrame: 100,
SystemTimeUTC: "2024-01-15T14:30:45.123",
MissionDate: "2035-06-15T06:00:00",
TimeMultiplier: 2.0,
MissionTime: 3600.5,
}

if err := b.RecordTimeState(state); err != nil {
t.Fatalf("RecordTimeState failed: %v", err)
}

if len(b.timeStates) != 1 {
t.Errorf("expected 1 state, got %d", len(b.timeStates))
}
if b.timeStates[0].CaptureFrame != 100 {
t.Errorf("expected CaptureFrame=100, got %d", b.timeStates[0].CaptureFrame)
}
if b.timeStates[0].TimeMultiplier != 2.0 {
t.Errorf("expected TimeMultiplier=2.0, got %f", b.timeStates[0].TimeMultiplier)
}
}

func TestRecordAce3DeathEvent(t *testing.T) {
b := New(config.MemoryConfig{})

Expand Down Expand Up @@ -664,6 +693,7 @@ func TestStartMissionResetsEverything(t *testing.T) {
_ = b.RecordChatEvent(&core.ChatEvent{})
_ = b.RecordRadioEvent(&core.RadioEvent{})
_ = b.RecordServerFpsEvent(&core.ServerFpsEvent{})
_ = b.RecordTimeState(&core.TimeState{})
_ = b.RecordAce3DeathEvent(&core.Ace3DeathEvent{})
_ = b.RecordAce3UnconsciousEvent(&core.Ace3UnconsciousEvent{})

Expand Down Expand Up @@ -699,6 +729,9 @@ func TestStartMissionResetsEverything(t *testing.T) {
if len(b.serverFpsEvents) != 0 {
t.Error("serverFpsEvents not reset")
}
if len(b.timeStates) != 0 {
t.Error("timeStates not reset")
}
if len(b.ace3DeathEvents) != 0 {
t.Error("ace3DeathEvents not reset")
}
Expand Down
1 change: 1 addition & 0 deletions internal/storage/storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ type Backend interface {
RecordChatEvent(e *core.ChatEvent) error
RecordRadioEvent(e *core.RadioEvent) error
RecordServerFpsEvent(e *core.ServerFpsEvent) error
RecordTimeState(t *core.TimeState) error
RecordAce3DeathEvent(e *core.Ace3DeathEvent) error
RecordAce3UnconsciousEvent(e *core.Ace3UnconsciousEvent) error

Expand Down
19 changes: 19 additions & 0 deletions internal/worker/dispatch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ type mockBackend struct {
chatEvents []*core.ChatEvent
radioEvents []*core.RadioEvent
fpsEvents []*core.ServerFpsEvent
timeStates []*core.TimeState
ace3Deaths []*core.Ace3DeathEvent
ace3Uncon []*core.Ace3UnconsciousEvent
initCalled bool
Expand Down Expand Up @@ -183,6 +184,13 @@ func (b *mockBackend) RecordServerFpsEvent(e *core.ServerFpsEvent) error {
return nil
}

func (b *mockBackend) RecordTimeState(t *core.TimeState) error {
b.mu.Lock()
defer b.mu.Unlock()
b.timeStates = append(b.timeStates, t)
return nil
}

func (b *mockBackend) RecordAce3DeathEvent(e *core.Ace3DeathEvent) error {
b.mu.Lock()
defer b.mu.Unlock()
Expand Down Expand Up @@ -247,6 +255,7 @@ type mockHandlerService struct {
chatEvent model.ChatEvent
radioEvent model.RadioEvent
fpsEvent model.ServerFpsEvent
timeState model.TimeState
ace3Death model.Ace3DeathEvent
ace3Uncon model.Ace3UnconsciousEvent
marker model.Marker
Expand Down Expand Up @@ -382,6 +391,16 @@ func (h *mockHandlerService) LogFpsEvent(args []string) (model.ServerFpsEvent, e
return h.fpsEvent, nil
}

func (h *mockHandlerService) LogTimeState(args []string) (model.TimeState, error) {
h.mu.Lock()
defer h.mu.Unlock()
h.calls = append(h.calls, "LogTimeState")
if h.returnError {
return model.TimeState{}, errors.New(h.errorMsg)
}
return h.timeState, nil
}

func (h *mockHandlerService) LogAce3DeathEvent(args []string) (model.Ace3DeathEvent, error) {
h.mu.Lock()
defer h.mu.Unlock()
Expand Down
1 change: 1 addition & 0 deletions internal/worker/worker.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ type HandlerService interface {
LogChatEvent(args []string) (model.ChatEvent, error)
LogRadioEvent(args []string) (model.RadioEvent, error)
LogFpsEvent(args []string) (model.ServerFpsEvent, error)
LogTimeState(args []string) (model.TimeState, error)
LogAce3DeathEvent(args []string) (model.Ace3DeathEvent, error)
LogAce3UnconsciousEvent(args []string) (model.Ace3UnconsciousEvent, error)
LogMarkerCreate(args []string) (model.Marker, error)
Expand Down
Loading