From b6131a7d3a6b104b1ab57b725ed93dd8897eb041 Mon Sep 17 00:00:00 2001 From: svalencia014 Date: Sat, 2 Aug 2025 22:20:04 -0400 Subject: [PATCH 1/3] add the ability to kick controllers --- pkg/client/control.go | 7 ++++ pkg/panes/messages.go | 5 +++ pkg/panes/stars/commands.go | 33 +++++++++++++++ pkg/server/dispatcher.go | 19 ++++++++- pkg/server/errors.go | 6 +++ pkg/server/manager.go | 83 +++++++++++++++++++++++++++++++++++++ pkg/sim/sim.go | 5 +++ ui.go | 40 ++++++++++++++++++ 8 files changed, 197 insertions(+), 1 deletion(-) diff --git a/pkg/client/control.go b/pkg/client/control.go index 58548abcc..d6daf7bc6 100644 --- a/pkg/client/control.go +++ b/pkg/client/control.go @@ -233,6 +233,13 @@ func (c *ControlClient) ChangeControlPosition(tcp string, keepTracks bool) error return err } +func (c *ControlClient) KickController(targetTCP string, callback func(error)) { + c.addCall(makeRPCCall(c.client.Go("Sim.KickController", &server.KickControllerArgs{ + ControllerToken: c.controllerToken, + TargetTCP: targetTCP, + }, nil, nil), callback)) +} + func (c *ControlClient) CreateDeparture(airport, runway, category string, rules av.FlightRules, ac *sim.Aircraft, callback func(error)) { c.addCall(makeRPCCall(c.client.Go("Sim.CreateDeparture", &server.CreateDepartureArgs{ diff --git a/pkg/panes/messages.go b/pkg/panes/messages.go index ee586dfce..212a04d64 100644 --- a/pkg/panes/messages.go +++ b/pkg/panes/messages.go @@ -223,6 +223,11 @@ func (mp *MessagesPane) processEvents(ctx *Context) { } case sim.StatusMessageEvent: + // Only show status messages if they're for us (when ToController is set) or for everyone (when ToController is empty) + if event.ToController != "" && event.ToController != ctx.UserTCP { + break + } + // Don't spam the same message repeatedly; look in the most recent 5. n := len(mp.messages) start := max(0, n-5) diff --git a/pkg/panes/stars/commands.go b/pkg/panes/stars/commands.go index 55b90234b..35fa12ed2 100644 --- a/pkg/panes/stars/commands.go +++ b/pkg/panes/stars/commands.go @@ -688,6 +688,39 @@ func (sp *STARSPane) executeSTARSCommand(ctx *panes.Context, cmd string, tracks return } + if len(cmd) >= 3 && cmd[:2] == "*K" { + // Kick controller command: *K + targetTCP := strings.TrimSpace(cmd[2:]) + if targetTCP == "" { + status.err = ErrSTARSCommandFormat + return + } + + // Any signed-in controller can kick others (no instructor/RPO restriction) + + // Verify the target controller exists and is signed in + if !slices.Contains(ctx.Client.State.HumanControllers, targetTCP) { + status.err = ErrSTARSIllegalPosition + return + } + + // Don't allow kicking yourself + if targetTCP == ctx.UserTCP { + status.err = ErrSTARSIllegalParam + return + } + + // Kick the controller + ctx.Client.KickController(targetTCP, func(err error) { + if err != nil { + sp.displayError(err, ctx, "") + } + }) + + status.clear = true + return + } + if len(cmd) > 3 && cmd[:3] == "*F " && sp.wipSignificantPoint != nil { if sig, ok := sp.significantPoints[cmd[3:]]; ok { status = sp.displaySignificantPointInfo(*sp.wipSignificantPoint, sig.Location, diff --git a/pkg/server/dispatcher.go b/pkg/server/dispatcher.go index 8db64c9e2..e788543a9 100644 --- a/pkg/server/dispatcher.go +++ b/pkg/server/dispatcher.go @@ -34,6 +34,17 @@ func (sd *dispatcher) SignOff(token string, _ *struct{}) error { return sd.sm.SignOff(token) } +type KickControllerArgs struct { + ControllerToken string + TargetTCP string +} + +func (sd *dispatcher) KickController(kc *KickControllerArgs, _ *struct{}) error { + defer sd.sm.lg.CatchAndReportCrash() + + return sd.sm.KickController(kc.ControllerToken, kc.TargetTCP) +} + type ChangeControlPositionArgs struct { ControllerToken string TCP string @@ -46,7 +57,13 @@ func (sd *dispatcher) ChangeControlPosition(cs *ChangeControlPositionArgs, _ *st if ctrl, s, ok := sd.sm.LookupController(cs.ControllerToken); !ok { return ErrNoSimForControllerToken } else { - return s.ChangeControlPosition(ctrl.tcp, cs.TCP, cs.KeepTracks) + oldTCP := ctrl.tcp + if err := s.ChangeControlPosition(ctrl.tcp, cs.TCP, cs.KeepTracks); err != nil { + return err + } + // Update the controller mappings in SimManager after successful position change + sd.sm.UpdateControllerPosition(cs.ControllerToken, oldTCP, cs.TCP) + return nil } } diff --git a/pkg/server/errors.go b/pkg/server/errors.go index 9b890dd61..f94e2cceb 100644 --- a/pkg/server/errors.go +++ b/pkg/server/errors.go @@ -23,6 +23,9 @@ var ( ErrRPCTimeout = errors.New("RPC call timed out") ErrRPCVersionMismatch = errors.New("Client and server RPC versions don't match") ErrServerDisconnected = errors.New("Server disconnected") + ErrInsufficientPermissions = errors.New("Insufficient permissions to perform this action") + ErrNoControllerToKick = errors.New("No controller found with that position") + ErrCannotKickSelf = errors.New("Cannot kick yourself") ) var errorStringToError = map[string]error{ @@ -87,6 +90,9 @@ var errorStringToError = map[string]error{ ErrRPCTimeout.Error(): ErrRPCTimeout, ErrRPCVersionMismatch.Error(): ErrRPCVersionMismatch, ErrServerDisconnected.Error(): ErrServerDisconnected, + ErrInsufficientPermissions.Error(): ErrInsufficientPermissions, + ErrNoControllerToKick.Error(): ErrNoControllerToKick, + ErrCannotKickSelf.Error(): ErrCannotKickSelf, } func TryDecodeError(e error) error { diff --git a/pkg/server/manager.go b/pkg/server/manager.go index d6fe838d5..9a1d8192b 100644 --- a/pkg/server/manager.go +++ b/pkg/server/manager.go @@ -520,6 +520,72 @@ func (sm *SimManager) SignOff(token string) error { return sm.signOff(token) } +func (sm *SimManager) KickController(adminToken, targetTCP string) error { + sm.mu.Lock(sm.lg) + defer sm.mu.Unlock(sm.lg) + + // First verify that the admin has permission to kick + adminCtrl, adminSim, ok := sm.lookupController(adminToken) + if !ok { + return ErrNoSimForControllerToken + } + + // Any signed-in controller can kick others (no instructor/RPO restriction) + + // Find the target controller in the same sim + var targetToken string + for tcp, ctrl := range adminCtrl.asim.controllersByTCP { + if tcp == targetTCP { + targetToken = ctrl.token + break + } + } + + if targetToken == "" { + return ErrNoControllerToKick + } + + // Don't allow kicking yourself + if targetToken == adminToken { + return ErrCannotKickSelf + } + + // Send immediate feedback to the admin + adminSim.PostEvent(sim.Event{ + Type: sim.StatusMessageEvent, + WrittenText: fmt.Sprintf("Kicking %s...", targetTCP), + ToController: adminCtrl.tcp, + }) + + // Send notification to the kicked controller + adminSim.PostEvent(sim.Event{ + Type: sim.StatusMessageEvent, + WrittenText: fmt.Sprintf("You have been kicked by %s", adminCtrl.tcp), + ToController: targetTCP, + }) + + // Perform the kick after a short delay to allow notification delivery + go func() { + time.Sleep(500 * time.Millisecond) // Give time for notification to be processed + + sm.mu.Lock(sm.lg) + defer sm.mu.Unlock(sm.lg) + + err := sm.signOff(targetToken) + if err == nil { + sm.lg.Infof("%s: kicked by %s", targetTCP, adminCtrl.tcp) + adminSim.PostEvent(sim.Event{ + Type: sim.StatusMessageEvent, + WrittenText: fmt.Sprintf("%s was kicked by %s", targetTCP, adminCtrl.tcp), + }) + } else { + sm.lg.Errorf("Failed to kick %s: %v", targetTCP, err) + } + }() + + return nil +} + func (sm *SimManager) signOff(token string) error { if ctrl, s, ok := sm.lookupController(token); !ok { return ErrNoSimForControllerToken @@ -597,6 +663,23 @@ func (sm *SimManager) lookupController(token string) (*humanController, *sim.Sim return nil, nil, false } +// UpdateControllerPosition updates the controller mappings after a position change +func (sm *SimManager) UpdateControllerPosition(token, oldTCP, newTCP string) { + sm.mu.Lock(sm.lg) + defer sm.mu.Unlock(sm.lg) + + if ctrl, ok := sm.controllersByToken[token]; ok { + // Remove from old TCP mapping + delete(ctrl.asim.controllersByTCP, oldTCP) + + // Update the controller's TCP field + ctrl.tcp = newTCP + + // Add to new TCP mapping + ctrl.asim.controllersByTCP[newTCP] = ctrl + } +} + const simIdleLimit = 4 * time.Hour func (sm *SimManager) SimShouldExit(sim *sim.Sim) bool { diff --git a/pkg/sim/sim.go b/pkg/sim/sim.go index 27d50abda..2b4975cfd 100644 --- a/pkg/sim/sim.go +++ b/pkg/sim/sim.go @@ -351,6 +351,11 @@ func (s *Sim) signOn(tcp string, instructor bool, disableTextToSpeech bool) erro s.State.Controllers[tcp] = s.SignOnPositions[tcp] s.State.HumanControllers = append(s.State.HumanControllers, tcp) + // Set instructor status if the flag is true + if instructor { + s.Instructors[tcp] = true + } + if tcp == s.State.PrimaryController { // The primary controller signed in so the sim will resume. // Reset lastUpdateTime so that the next time Update() is diff --git a/ui.go b/ui.go index 640825186..74a853814 100644 --- a/ui.go +++ b/ui.go @@ -300,6 +300,18 @@ func uiDraw(mgr *client.ConnectionManager, config *Config, p platform.Platform, for _, event := range ui.eventsSubscription.Get() { if event.Type == sim.ServerBroadcastMessageEvent { uiShowModalDialog(NewModalDialogBox(&BroadcastModalDialog{Message: event.WrittenText}, p), false) + } else if event.Type == sim.StatusMessageEvent && event.ToController != "" && controlClient != nil && event.ToController == controlClient.State.UserTCP { + // Check if this is a kick notification + if strings.Contains(event.WrittenText, "You have been kicked by") { + client := &KickNotificationModalClient{ + message: event.WrittenText, + mgr: mgr, + config: config, + platform: p, + logger: lg, + } + uiShowModalDialog(NewModalDialogBox(client, p), true) // Show at front for visibility + } } } @@ -944,6 +956,34 @@ func (e *ErrorModalClient) Draw() int { return -1 } +type KickNotificationModalClient struct { + message string + mgr *client.ConnectionManager + config *Config + platform platform.Platform + logger *log.Logger +} + +func (k *KickNotificationModalClient) Title() string { return "Kicked from Simulation" } +func (k *KickNotificationModalClient) Opening() {} + +func (k *KickNotificationModalClient) Buttons() []ModalDialogButton { + return []ModalDialogButton{{ + text: "Ok", + action: func() bool { + // When the user clicks OK, show the connection dialog + uiShowConnectDialog(k.mgr, true, k.config, k.platform, k.logger) + return true + }, + }} +} + +func (k *KickNotificationModalClient) Draw() int { + text, _ := util.WrapText(k.message, 80, 0, true) + imgui.Text("\n\n" + text + "\n\n") + return -1 +} + func ShowErrorDialog(p platform.Platform, lg *log.Logger, s string, args ...interface{}) { d := NewModalDialogBox(&ErrorModalClient{message: fmt.Sprintf(s, args...)}, p) uiShowModalDialog(d, true) From 77d6e473b91333d63c9f2bdbdafa96bf989a6454 Mon Sep 17 00:00:00 2001 From: svalencia014 Date: Sat, 2 Aug 2025 22:23:25 -0400 Subject: [PATCH 2/3] run go fmt --- ui.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ui.go b/ui.go index 74a853814..7d53d4df3 100644 --- a/ui.go +++ b/ui.go @@ -304,11 +304,11 @@ func uiDraw(mgr *client.ConnectionManager, config *Config, p platform.Platform, // Check if this is a kick notification if strings.Contains(event.WrittenText, "You have been kicked by") { client := &KickNotificationModalClient{ - message: event.WrittenText, - mgr: mgr, - config: config, + message: event.WrittenText, + mgr: mgr, + config: config, platform: p, - logger: lg, + logger: lg, } uiShowModalDialog(NewModalDialogBox(client, p), true) // Show at front for visibility } @@ -969,7 +969,7 @@ func (k *KickNotificationModalClient) Opening() {} func (k *KickNotificationModalClient) Buttons() []ModalDialogButton { return []ModalDialogButton{{ - text: "Ok", + text: "Ok", action: func() bool { // When the user clicks OK, show the connection dialog uiShowConnectDialog(k.mgr, true, k.config, k.platform, k.logger) From 9f00585d61366c787afb9a211c3f881eb1ef70d9 Mon Sep 17 00:00:00 2001 From: svalencia014 Date: Sat, 2 Aug 2025 22:27:21 -0400 Subject: [PATCH 3/3] run gofmt again --- pkg/panes/messages.go | 2 +- pkg/server/manager.go | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pkg/panes/messages.go b/pkg/panes/messages.go index 212a04d64..15e9c3f15 100644 --- a/pkg/panes/messages.go +++ b/pkg/panes/messages.go @@ -227,7 +227,7 @@ func (mp *MessagesPane) processEvents(ctx *Context) { if event.ToController != "" && event.ToController != ctx.UserTCP { break } - + // Don't spam the same message repeatedly; look in the most recent 5. n := len(mp.messages) start := max(0, n-5) diff --git a/pkg/server/manager.go b/pkg/server/manager.go index 9a1d8192b..78af5173b 100644 --- a/pkg/server/manager.go +++ b/pkg/server/manager.go @@ -552,8 +552,8 @@ func (sm *SimManager) KickController(adminToken, targetTCP string) error { // Send immediate feedback to the admin adminSim.PostEvent(sim.Event{ - Type: sim.StatusMessageEvent, - WrittenText: fmt.Sprintf("Kicking %s...", targetTCP), + Type: sim.StatusMessageEvent, + WrittenText: fmt.Sprintf("Kicking %s...", targetTCP), ToController: adminCtrl.tcp, }) @@ -567,10 +567,10 @@ func (sm *SimManager) KickController(adminToken, targetTCP string) error { // Perform the kick after a short delay to allow notification delivery go func() { time.Sleep(500 * time.Millisecond) // Give time for notification to be processed - + sm.mu.Lock(sm.lg) defer sm.mu.Unlock(sm.lg) - + err := sm.signOff(targetToken) if err == nil { sm.lg.Infof("%s: kicked by %s", targetTCP, adminCtrl.tcp) @@ -671,10 +671,10 @@ func (sm *SimManager) UpdateControllerPosition(token, oldTCP, newTCP string) { if ctrl, ok := sm.controllersByToken[token]; ok { // Remove from old TCP mapping delete(ctrl.asim.controllersByTCP, oldTCP) - + // Update the controller's TCP field ctrl.tcp = newTCP - + // Add to new TCP mapping ctrl.asim.controllersByTCP[newTCP] = ctrl }