From 96e2b53b5c83ca5715411fb7d9d8b22fe4f16454 Mon Sep 17 00:00:00 2001 From: Peter van der Meer Date: Tue, 14 Jan 2020 02:51:30 +0100 Subject: [PATCH] refactor(input-handling): Move kb/m reads from AI & Engine to only Engine (#4715) * Add an "And" function to the Command class. * Move keyboard inputs from AI to Engine Combine mouse and keyboard inputs into single activeCommands variable Tested (OK, no issues seen): MENU MAP INFO FORWARD, LEFT, RIGHT, BACK, PRIMARY, AFTERBURNER SCAN DEPLOY HAIL BOARD CLOAK LAND (autopilot) LAND (switch landing targets) LAND (landing based on mouseclick) JUMP (autopilot) JUMP (insufficient fuel warning) JUMP (WAIT, fleet waiting for jump) TARGET (cycle through enemy ships) NEAREST (cycle through friendly ships) SELECT (switch secondary weapon) SECONDARY (fire secondary weapon) DEPLOY FIGHT GATHER (looks the same as before the change) HOLD (looks the same as before the change) AMMO Signed-off-by: Peter van der Meer * Add comments on Command and made comments full sentences. - Documented the use of the WAIT command. - Made all comments full sentences, included full stops. * Fixed clearing commands from the wrong variable. * Remove duplicate audio-repeat-avoidance construct Repeatedly playing audio was already resolved by forwarding autoPilot commands only 1 single frame through activeCommands and by clearing the autoPilot at the same time as giving the error-message and playing the fail sound. * Fix an issue where JUMP is cleared before out-of-fuel error-message. The targetSystem gets set during emergency jumps (without a travel plan). If such situation happens and there is insufficient fuel or no jump or hyperdrive, then we still want the warning to be sent on the first time the jump command is given. --- source/AI.cpp | 99 +++++++++++++++++++++------------------------- source/AI.h | 10 +---- source/Command.cpp | 8 ++++ source/Command.h | 10 ++++- source/Engine.cpp | 48 ++++++++++++++++++++-- source/Engine.h | 11 +++++- 6 files changed, 116 insertions(+), 70 deletions(-) diff --git a/source/AI.cpp b/source/AI.cpp index 327c72a2..d090402f 100644 --- a/source/AI.cpp +++ b/source/AI.cpp @@ -276,22 +276,18 @@ void AI::IssueMoveTarget(const PlayerInfo &player, const Point &target, const Sy // Commands issued via the keyboard (mostly, to the flagship). -void AI::UpdateKeys(PlayerInfo &player, Command &clickCommands, bool isActive) +void AI::UpdateKeys(PlayerInfo &player, Command &activeCommands, bool isActive) { shift = (SDL_GetModState() & KMOD_SHIFT); escortsUseAmmo = Preferences::Has("Escorts expend ammo"); escortsAreFrugal = Preferences::Has("Escorts use ammo frugally"); - Command oldHeld = keyHeld; - keyHeld.ReadKeyboard(); - autoPilot |= clickCommands; - clickCommands.Clear(); - keyDown = keyHeld.AndNot(oldHeld); - if(keyHeld.Has(AutopilotCancelCommands())) + autoPilot |= activeCommands; + if(activeCommands.Has(AutopilotCancelCommands())) { - bool canceled = (autoPilot.Has(Command::JUMP) && !keyHeld.Has(Command::JUMP)); - canceled |= (autoPilot.Has(Command::LAND) && !keyHeld.Has(Command::LAND)); - canceled |= (autoPilot.Has(Command::BOARD) && !keyHeld.Has(Command::BOARD)); + bool canceled = (autoPilot.Has(Command::JUMP) && !activeCommands.Has(Command::JUMP)); + canceled |= (autoPilot.Has(Command::LAND) && !activeCommands.Has(Command::LAND)); + canceled |= (autoPilot.Has(Command::BOARD) && !activeCommands.Has(Command::BOARD)); if(canceled) Messages::Add("Disengaging autopilot."); autoPilot.Clear(); @@ -301,12 +297,8 @@ void AI::UpdateKeys(PlayerInfo &player, Command &clickCommands, bool isActive) if(!isActive || !flagship || flagship->IsDestroyed()) return; - ++landKeyInterval; - if(oldHeld.Has(Command::LAND)) - landKeyInterval = 0; - // Only toggle the "cloak" command if one of your ships has a cloaking device. - if(keyDown.Has(Command::CLOAK)) + if(activeCommands.Has(Command::CLOAK)) for(const auto &it : player.Ships()) if(!it->IsParked() && it->Attributes().Get("cloak")) { @@ -316,7 +308,7 @@ void AI::UpdateKeys(PlayerInfo &player, Command &clickCommands, bool isActive) } // Toggle your secondary weapon. - if(keyDown.Has(Command::SELECT)) + if(activeCommands.Has(Command::SELECT)) player.SelectNext(); // The commands below here only apply if you have escorts or fighters. @@ -324,7 +316,7 @@ void AI::UpdateKeys(PlayerInfo &player, Command &clickCommands, bool isActive) return; // Only toggle the "deploy" command if one of your ships has fighter bays. - if(keyDown.Has(Command::DEPLOY)) + if(activeCommands.Has(Command::DEPLOY)) for(const auto &it : player.Ships()) if(it->HasBays()) { @@ -335,23 +327,24 @@ void AI::UpdateKeys(PlayerInfo &player, Command &clickCommands, bool isActive) shared_ptr target = flagship->GetTargetShip(); Orders newOrders; - if(keyDown.Has(Command::FIGHT) && target && !target->IsYours()) + if(activeCommands.Has(Command::FIGHT) && target && !target->IsYours()) { newOrders.type = target->IsDisabled() ? Orders::FINISH_OFF : Orders::ATTACK; newOrders.target = target; IssueOrders(player, newOrders, "focusing fire on \"" + target->Name() + "\"."); } - if(keyDown.Has(Command::HOLD)) + if(activeCommands.Has(Command::HOLD)) { newOrders.type = Orders::HOLD_POSITION; IssueOrders(player, newOrders, "holding position."); } - if(keyDown.Has(Command::GATHER)) + if(activeCommands.Has(Command::GATHER)) { newOrders.type = Orders::GATHER; newOrders.target = player.FlagshipPtr(); IssueOrders(player, newOrders, "gathering around your flagship."); } + // Get rid of any invalid orders. Carried ships will retain orders in case they are deployed. for(auto it = orders.begin(); it != orders.end(); ) { @@ -428,7 +421,7 @@ void AI::ClearOrders() -void AI::Step(const PlayerInfo &player) +void AI::Step(const PlayerInfo &player, Command &activeCommands) { // First, figure out the comparative strengths of the present governments. const System *playerSystem = player.GetSystem(); @@ -469,7 +462,7 @@ void AI::Step(const PlayerInfo &player) if(it.get() == flagship) { - MovePlayer(*it, player); + MovePlayer(*it, player, activeCommands); continue; } @@ -3003,7 +2996,7 @@ double AI::RendezvousTime(const Point &p, const Point &v, double vp) -void AI::MovePlayer(Ship &ship, const PlayerInfo &player) +void AI::MovePlayer(Ship &ship, const PlayerInfo &player, Command &activeCommands) { Command command; @@ -3092,7 +3085,7 @@ void AI::MovePlayer(Ship &ship, const PlayerInfo &player) ship.SetTargetStellar(system->FindStellar(bestDestination)); } - if(keyDown.Has(Command::NEAREST)) + if(activeCommands.Has(Command::NEAREST)) { // Find the nearest ship to the flagship. If `Shift` is held, consider friendly ships too. double closest = numeric_limits::infinity(); @@ -3138,7 +3131,7 @@ void AI::MovePlayer(Ship &ship, const PlayerInfo &player) } } } - else if(keyDown.Has(Command::TARGET)) + else if(activeCommands.Has(Command::TARGET)) { // Find the "next" ship to target. Holding `Shift` will cycle through escorts. shared_ptr target = ship.GetTargetShip(); @@ -3164,7 +3157,7 @@ void AI::MovePlayer(Ship &ship, const PlayerInfo &player) if(selectNext) ship.SetTargetShip(shared_ptr()); } - else if(keyDown.Has(Command::BOARD)) + else if(activeCommands.Has(Command::BOARD)) { // Board the nearest disabled ship, focusing on hostiles before allies. Holding // `Shift` results in boarding only player-owned escorts in need of assistance. @@ -3194,10 +3187,10 @@ void AI::MovePlayer(Ship &ship, const PlayerInfo &player) } } if(!foundAnything) - keyDown.Clear(Command::BOARD); + activeCommands.Clear(Command::BOARD); } } - else if(keyDown.Has(Command::LAND) && !ship.IsEnteringHyperspace()) + else if(activeCommands.Has(Command::LAND) && !ship.IsEnteringHyperspace()) { // Track all possible landable objects in the current system. auto landables = vector{}; @@ -3231,7 +3224,7 @@ void AI::MovePlayer(Ship &ship, const PlayerInfo &player) // selected, then press "land" again, do not toggle to the other if // you are within landing range of the one you have selected. } - else if(message.empty() && target && landKeyInterval < 60) + else if(message.empty() && target && activeCommands.Has(Command::WAIT)) { // Select the next landable in the list after the currently selected object. if(++landIt == landables.cend()) @@ -3311,7 +3304,7 @@ void AI::MovePlayer(Ship &ship, const PlayerInfo &player) if(!message.empty()) Messages::Add(message); } - else if(keyDown.Has(Command::JUMP)) + else if(activeCommands.Has(Command::JUMP)) { if(!ship.GetTargetSystem() && !isWormhole) { @@ -3344,30 +3337,30 @@ void AI::MovePlayer(Ship &ship, const PlayerInfo &player) Messages::Add("Engaging autopilot to jump to the " + name + " system."); } } - else if(keyHeld.Has(Command::SCAN)) + else if(activeCommands.Has(Command::SCAN)) command |= Command::SCAN; const shared_ptr target = ship.GetTargetShip(); AimTurrets(ship, command, !Preferences::Has("Turrets focus fire")); if(Preferences::Has("Automatic firing") && !ship.IsBoarding() - && !(autoPilot | keyHeld).Has(Command::LAND | Command::JUMP | Command::BOARD) + && !(autoPilot | activeCommands).Has(Command::LAND | Command::JUMP | Command::BOARD) && (!target || target->GetGovernment()->IsEnemy())) AutoFire(ship, command, false); - if(keyHeld) + if(activeCommands) { - if(keyHeld.Has(Command::FORWARD)) + if(activeCommands.Has(Command::FORWARD)) command |= Command::FORWARD; - if(keyHeld.Has(Command::RIGHT | Command::LEFT)) - command.SetTurn(keyHeld.Has(Command::RIGHT) - keyHeld.Has(Command::LEFT)); - if(keyHeld.Has(Command::BACK)) + if(activeCommands.Has(Command::RIGHT | Command::LEFT)) + command.SetTurn(activeCommands.Has(Command::RIGHT) - activeCommands.Has(Command::LEFT)); + if(activeCommands.Has(Command::BACK)) { - if(!keyHeld.Has(Command::FORWARD) && ship.Attributes().Get("reverse thrust")) + if(!activeCommands.Has(Command::FORWARD) && ship.Attributes().Get("reverse thrust")) command |= Command::BACK; - else if(!keyHeld.Has(Command::RIGHT | Command::LEFT)) + else if(!activeCommands.Has(Command::RIGHT | Command::LEFT)) command.SetTurn(TurnBackward(ship)); } - if(keyHeld.Has(Command::PRIMARY)) + if(activeCommands.Has(Command::PRIMARY)) { int index = 0; for(const Hardpoint &hardpoint : ship.Weapons()) @@ -3377,7 +3370,7 @@ void AI::MovePlayer(Ship &ship, const PlayerInfo &player) ++index; } } - if(keyHeld.Has(Command::SECONDARY)) + if(activeCommands.Has(Command::SECONDARY)) { int index = 0; for(const Hardpoint &hardpoint : ship.Weapons()) @@ -3387,15 +3380,15 @@ void AI::MovePlayer(Ship &ship, const PlayerInfo &player) ++index; } } - if(keyHeld.Has(Command::AFTERBURNER)) + if(activeCommands.Has(Command::AFTERBURNER)) command |= Command::AFTERBURNER; - if(keyHeld.Has(AutopilotCancelCommands())) - autoPilot = keyHeld; + if(activeCommands.Has(AutopilotCancelCommands())) + autoPilot = activeCommands; } bool shouldAutoAim = false; if(Preferences::Has("Automatic aiming") && !command.Turn() && !ship.IsBoarding() - && (Preferences::Has("Automatic firing") || keyHeld.Has(Command::PRIMARY)) + && (Preferences::Has("Automatic firing") || activeCommands.Has(Command::PRIMARY)) && ((target && target->GetSystem() == ship.GetSystem() && target->IsTargetable()) || ship.GetTargetAsteroid()) && !autoPilot.Has(Command::LAND | Command::JUMP | Command::BOARD)) @@ -3415,7 +3408,7 @@ void AI::MovePlayer(Ship &ship, const PlayerInfo &player) command.SetTurn(TurnToward(ship, TargetAim(ship))); } - if(autoPilot.Has(Command::JUMP) && !player.HasTravelPlan()) + if(autoPilot.Has(Command::JUMP) && !(player.HasTravelPlan() || ship.GetTargetSystem())) { // The player completed their travel plan, which may have indicated a destination within the final system. autoPilot.Clear(Command::JUMP); @@ -3439,7 +3432,7 @@ void AI::MovePlayer(Ship &ship, const PlayerInfo &player) if(autoPilot.Has(Command::LAND) || (autoPilot.Has(Command::JUMP) && isWormhole)) { if(ship.GetPlanet()) - autoPilot.Clear(); + autoPilot.Clear(Command::LAND | Command::JUMP); else { MoveToPlanet(ship, command); @@ -3448,33 +3441,29 @@ void AI::MovePlayer(Ship &ship, const PlayerInfo &player) } else if(autoPilot.Has(Command::JUMP)) { - bool isNewPress = keyDown.Has(Command::JUMP) || !keyHeld.Has(Command::JUMP); if(!ship.Attributes().Get("hyperdrive") && !ship.Attributes().Get("jump drive")) { Messages::Add("You do not have a hyperdrive installed."); autoPilot.Clear(); - if(isNewPress) - Audio::Play(Audio::Get("fail")); + Audio::Play(Audio::Get("fail")); } else if(!ship.JumpFuel(ship.GetTargetSystem())) { Messages::Add("You cannot jump to the selected system."); autoPilot.Clear(); - if(isNewPress) - Audio::Play(Audio::Get("fail")); + Audio::Play(Audio::Get("fail")); } else if(!ship.JumpsRemaining() && !ship.IsEnteringHyperspace()) { Messages::Add("You do not have enough fuel to make a hyperspace jump."); autoPilot.Clear(); - if(isNewPress) - Audio::Play(Audio::Get("fail")); + Audio::Play(Audio::Get("fail")); } else { PrepareForHyperspace(ship, command); command |= Command::JUMP; - if(keyHeld.Has(Command::JUMP)) + if(activeCommands.Has(Command::WAIT)) command |= Command::WAIT; } } diff --git a/source/AI.h b/source/AI.h index ba7c554a..2b081205 100644 --- a/source/AI.h +++ b/source/AI.h @@ -64,7 +64,7 @@ template // but not when they jump from one system to another. void ClearOrders(); // Issue AI commands to all ships for one game step. - void Step(const PlayerInfo &player); + void Step(const PlayerInfo &player, Command &activeCommands); // Get the in-system strength of each government's allies and enemies. int64_t AllyStrength(const Government *government); @@ -133,7 +133,7 @@ template // projectile. If it cannot hit the target, this returns NaN. static double RendezvousTime(const Point &p, const Point &v, double vp); - void MovePlayer(Ship &ship, const PlayerInfo &player); + void MovePlayer(Ship &ship, const PlayerInfo &player, Command &activeCommands); // True if the ship performed the indicated event to the other ship. bool Has(const Ship &ship, const std::weak_ptr &other, int type) const; @@ -183,10 +183,6 @@ template // helps limit how often certain actions occur (such as changing targets). int step = 0; - // Commands that are newly active for this step. - Command keyDown; - // Commands that are active for this step. - Command keyHeld; // Command applied by the player's "autopilot." Command autoPilot; @@ -197,8 +193,6 @@ template bool escortsAreFrugal = true; bool escortsUseAmmo = true; - // Pressing "land" rapidly toggles targets; pressing it once re-engages landing. - int landKeyInterval = 0; // Current orders for the player's ships. Because this map only applies to // player ships, which are never deleted except when landed, it can use diff --git a/source/Command.cpp b/source/Command.cpp index fa337334..72890270 100644 --- a/source/Command.cpp +++ b/source/Command.cpp @@ -247,6 +247,14 @@ bool Command::Has(Command command) const +// Get the commands that are set in this and in the given command. +Command Command::And(Command command) const +{ + return Command(state & command.state); +} + + + // Get the commands that are set in this and not in the given command. Command Command::AndNot(Command command) const { diff --git a/source/Command.h b/source/Command.h index 00782401..2fdd0eea 100644 --- a/source/Command.h +++ b/source/Command.h @@ -55,8 +55,12 @@ class Command { static const Command GATHER; static const Command HOLD; static const Command AMMO; - // This command from the AI tells a ship not to jump or land yet even if it - // is in position to do so. (There is no key mapped to this command.) + // This command is given in combination with JUMP or LAND and tells a ship + // not to jump or land yet even if it is in position to do so. It can be + // given from the AI when a ship is waiting for its parent. It can also be + // given from the player/input engine when the player is preparing his/her + // fleet for jumping or to indicate that the player is switching landing + // targets. (There is no explicit key mapped to this command.) static const Command WAIT; // This command from the AI tells a ship that if possible, it should apply // less than its full thrust in order to come to a complete stop. @@ -93,6 +97,8 @@ class Command { void Clear(Command command); void Set(Command command); bool Has(Command command) const; + // Get the commands that are set in this and in the given command. + Command And(Command command) const; // Get the commands that are set in this and not in the given command. Command AndNot(Command command) const; diff --git a/source/Engine.cpp b/source/Engine.cpp index e304ef98..592d74e8 100644 --- a/source/Engine.cpp +++ b/source/Engine.cpp @@ -433,7 +433,8 @@ void Engine::Step(bool isActive) --jumpCount; } ai.UpdateEvents(events); - ai.UpdateKeys(player, clickCommands, isActive && wasActive); + HandleKeyboardInputs(); + ai.UpdateKeys(player, activeCommands, isActive && wasActive); wasActive = isActive; Audio::Update(center); @@ -1212,7 +1213,10 @@ void Engine::CalculateStep() return; // Now, all the ships must decide what they are doing next. - ai.Step(player); + ai.Step(player, activeCommands); + + // Clear the active players commands, they are all processed at this point. + activeCommands.Clear(); // Perform actions for all the game objects. In general this is ordered from // bottom to top of the draw stack, but in some cases one object type must @@ -1578,6 +1582,42 @@ void Engine::SendHails() +// Handle any keyboard inputs for the engine. This is done in the main thread +// after all calculation threads are paused to avoid race conditions. +void Engine::HandleKeyboardInputs() +{ + Ship *flagship = player.Flagship(); + + // Commands can't be issued if your flagship is dead. + if(!flagship || flagship->IsDestroyed()) + return; + + // Determine which new keys were pressed by the player. + Command oldHeld = keyHeld; + keyHeld.ReadKeyboard(); + Command keyDown = keyHeld.AndNot(oldHeld); + + ++landKeyInterval; + if(oldHeld.Has(Command::LAND)) + landKeyInterval = 0; + + // Wait with actual jumping until the jump command is released. + // Or if pressing land quicky in succession, then use WAIT to switch landing targets. + if(keyHeld.Has(Command::JUMP) || (keyHeld.Has(Command::LAND) && landKeyInterval < 60)) + activeCommands.Set(Command::WAIT); + else + activeCommands.Clear(Command::WAIT); + + // Transfer all commands that need to be active as long as the corresponding key is pressed. + activeCommands |= (keyHeld.And(Command::PRIMARY | Command::SECONDARY | Command::SCAN | + Command::FORWARD | Command::LEFT | Command::RIGHT | Command::BACK | Command::AFTERBURNER)); + + // Transfer all newly pressed unhandled keys to active commands. + activeCommands |= keyDown; +} + + + // Handle any mouse clicks. This is done in the calculation thread rather than // in the main UI thread to avoid race conditions. void Engine::HandleMouseClicks() @@ -1607,7 +1647,7 @@ void Engine::HandleMouseClicks() + " refuse to let you land."); else { - clickCommands |= Command::LAND; + activeCommands |= Command::LAND; Messages::Add("Landing on " + planet->Name() + "."); } } @@ -1644,7 +1684,7 @@ void Engine::HandleMouseClicks() { // Left click: has your flagship select or board the target. if(clickTarget == flagship->GetTargetShip()) - clickCommands |= Command::BOARD; + activeCommands |= Command::BOARD; else { flagship->SetTargetShip(clickTarget); diff --git a/source/Engine.h b/source/Engine.h index 58801e52..fc609558 100644 --- a/source/Engine.h +++ b/source/Engine.h @@ -96,6 +96,7 @@ class Engine { void SpawnFleets(); void SpawnPersons(); void SendHails(); + void HandleKeyboardInputs(); void HandleMouseClicks(); void FillCollisionSets(); @@ -201,6 +202,15 @@ class Engine { bool doEnter = false; bool hadHostiles = false; + // Commands that are currently active (and not yet handled). This is a combination + // of keyboard and mouse commands (and any other available input device). + Command activeCommands; + // Keyboard commands that were active in the previous step. + Command keyHeld; + // Pressing "land" rapidly toggles targets; pressing it once re-engages landing. + int landKeyInterval = 0; + + // Inputs received from a mouse or other pointer device. bool doClickNextStep = false; bool doClick = false; bool hasShift = false; @@ -210,7 +220,6 @@ class Engine { Point clickPoint; Rectangle clickBox; int groupSelect = -1; - Command clickCommands; double zoom = 1.;