1 // Copyright © 2008-2021 Pioneer Developers. See AUTHORS.txt for details
2 // Licensed under the terms of the GPL v3. See licenses/GPL-3.txt
3
4 #include "PlayerShipController.h"
5 #include "Frame.h"
6 #include "Game.h"
7 #include "GameConfig.h"
8 #include "GameSaveError.h"
9 #include "Input.h"
10 #include "Pi.h"
11 #include "Player.h"
12 #include "Ship.h"
13 #include "Space.h"
14 #include "WorldView.h"
15 #include "core/OS.h"
16 #include "lua/LuaObject.h"
17
18 #include <algorithm>
19
REGISTER_INPUT_BINDING(PlayerShipController)20 REGISTER_INPUT_BINDING(PlayerShipController)
21 {
22 using namespace InputBindings;
23 auto controlsPage = Pi::input->GetBindingPage("ShipControls");
24
25 auto weaponsGroup = controlsPage->GetBindingGroup("Weapons");
26 input->AddActionBinding("BindTargetObject", weaponsGroup, Action({ SDLK_y }));
27 input->AddActionBinding("BindPrimaryFire", weaponsGroup, Action({ SDLK_SPACE }));
28 input->AddActionBinding("BindSecondaryFire", weaponsGroup, Action({ SDLK_m }));
29
30 auto flightGroup = controlsPage->GetBindingGroup("ShipOrient");
31 input->AddAxisBinding("BindAxisPitch", flightGroup, Axis({}, { SDLK_k }, { SDLK_i }));
32 input->AddAxisBinding("BindAxisYaw", flightGroup, Axis({}, { SDLK_j }, { SDLK_l }));
33 input->AddAxisBinding("BindAxisRoll", flightGroup, Axis({}, { SDLK_u }, { SDLK_o }));
34 input->AddActionBinding("BindKillRot", flightGroup, Action({ SDLK_p }, { SDLK_x }));
35 input->AddActionBinding("BindToggleRotationDamping", flightGroup, Action({ SDLK_v }));
36
37 auto thrustGroup = controlsPage->GetBindingGroup("ManualControl");
38 input->AddAxisBinding("BindAxisThrustForward", thrustGroup, Axis({}, { SDLK_w }, { SDLK_s }));
39 input->AddAxisBinding("BindAxisThrustUp", thrustGroup, Axis({}, { SDLK_r }, { SDLK_f }));
40 input->AddAxisBinding("BindAxisThrustLeft", thrustGroup, Axis({}, { SDLK_a }, { SDLK_d }));
41 input->AddActionBinding("BindThrustLowPower", thrustGroup, Action({ SDLK_LSHIFT }));
42
43 auto speedGroup = controlsPage->GetBindingGroup("SpeedControl");
44 input->AddAxisBinding("BindSpeedControl", speedGroup, Axis({}, { SDLK_RETURN }, { SDLK_RSHIFT }));
45 input->AddActionBinding("BindToggleSetSpeed", speedGroup, Action({ SDLK_v }));
46 }
47
PlayerShipController()48 PlayerShipController::PlayerShipController() :
49 ShipController(),
50 InputBindings(Pi::input),
51 m_combatTarget(0),
52 m_navTarget(0),
53 m_setSpeedTarget(0),
54 m_controlsLocked(false),
55 m_invertMouse(false),
56 m_mouseActive(false),
57 m_disableMouseFacing(false),
58 m_rotationDamping(true),
59 m_mouseX(0.0),
60 m_mouseY(0.0),
61 m_setSpeed(0.0),
62 m_flightControlState(CONTROL_MANUAL),
63 m_lowThrustPower(0.25), // note: overridden by the default value in GameConfig.cpp (DefaultLowThrustPower setting)
64 m_mouseDir(0.0)
65 {
66 const float deadzone = Pi::config->Float("JoystickDeadzone");
67 m_joystickDeadzone = Clamp(deadzone, 0.01f, 1.0f); // do not use (deadzone * deadzone) as values are 0<>1 range, aka: 0.1 * 0.1 = 0.01 or 1% deadzone!!! Not what player asked for!
68 m_fovY = Pi::config->Float("FOVVertical");
69 m_lowThrustPower = Pi::config->Float("DefaultLowThrustPower");
70
71 InputBindings.RegisterBindings();
72 Pi::input->AddInputFrame(&InputBindings);
73
74 m_connRotationDampingToggleKey = InputBindings.toggleRotationDamping->onPressed.connect(
75 sigc::mem_fun(this, &PlayerShipController::ToggleRotationDamping));
76
77 m_fireMissileKey = InputBindings.secondaryFire->onPressed.connect(
78 sigc::mem_fun(this, &PlayerShipController::FireMissile));
79
80 m_setSpeedMode = InputBindings.toggleSetSpeed->onPressed.connect(
81 sigc::mem_fun(this, &PlayerShipController::ToggleSetSpeedMode));
82 }
83
RegisterBindings()84 void PlayerShipController::InputBinding::RegisterBindings()
85 {
86 targetObject = AddAction("BindTargetObject");
87 primaryFire = AddAction("BindPrimaryFire");
88 secondaryFire = AddAction("BindSecondaryFire");
89
90 pitch = AddAxis("BindAxisPitch");
91 yaw = AddAxis("BindAxisYaw");
92 roll = AddAxis("BindAxisRoll");
93
94 killRot = AddAction("BindKillRot");
95 toggleRotationDamping = AddAction("BindToggleRotationDamping");
96
97 thrustForward = AddAxis("BindAxisThrustForward");
98 thrustLeft = AddAxis("BindAxisThrustLeft");
99 thrustUp = AddAxis("BindAxisThrustUp");
100 thrustLowPower = AddAction("BindThrustLowPower");
101
102 speedControl = AddAxis("BindSpeedControl");
103 toggleSetSpeed = AddAction("BindToggleSetSpeed");
104 }
105
~PlayerShipController()106 PlayerShipController::~PlayerShipController()
107 {
108 m_setSpeedMode.disconnect();
109 m_fireMissileKey.disconnect();
110 m_connRotationDampingToggleKey.disconnect();
111
112 Pi::input->RemoveInputFrame(&InputBindings);
113 m_connRotationDampingToggleKey.disconnect();
114 m_fireMissileKey.disconnect();
115 }
116
SaveToJson(Json & jsonObj,Space * space)117 void PlayerShipController::SaveToJson(Json &jsonObj, Space *space)
118 {
119 Json playerShipControllerObj({}); // Create JSON object to contain player ship controller data.
120 playerShipControllerObj["flight_control_state"] = m_flightControlState;
121 playerShipControllerObj["set_speed"] = m_setSpeed;
122 playerShipControllerObj["low_thrust_power"] = m_lowThrustPower;
123 playerShipControllerObj["rotation_damping"] = m_rotationDamping;
124 playerShipControllerObj["index_for_combat_target"] = space->GetIndexForBody(m_combatTarget);
125 playerShipControllerObj["index_for_nav_target"] = space->GetIndexForBody(m_navTarget);
126 playerShipControllerObj["index_for_set_speed_target"] = space->GetIndexForBody(m_setSpeedTarget);
127 jsonObj["player_ship_controller"] = playerShipControllerObj; // Add player ship controller object to supplied object.
128 }
129
LoadFromJson(const Json & jsonObj)130 void PlayerShipController::LoadFromJson(const Json &jsonObj)
131 {
132 try {
133 Json playerShipControllerObj = jsonObj["player_ship_controller"];
134
135 m_flightControlState = playerShipControllerObj["flight_control_state"];
136 m_setSpeed = playerShipControllerObj["set_speed"];
137 m_lowThrustPower = playerShipControllerObj["low_thrust_power"];
138 m_rotationDamping = playerShipControllerObj["rotation_damping"];
139 //figure out actual bodies in PostLoadFixup - after Space body index has been built
140 m_combatTargetIndex = playerShipControllerObj["index_for_combat_target"];
141 m_navTargetIndex = playerShipControllerObj["index_for_nav_target"];
142 m_setSpeedTargetIndex = playerShipControllerObj["index_for_set_speed_target"];
143 } catch (Json::type_error &) {
144 throw SavedGameCorruptException();
145 }
146 }
147
PostLoadFixup(Space * space)148 void PlayerShipController::PostLoadFixup(Space *space)
149 {
150 m_combatTarget = space->GetBodyByIndex(m_combatTargetIndex);
151 m_navTarget = space->GetBodyByIndex(m_navTargetIndex);
152 m_setSpeedTarget = space->GetBodyByIndex(m_setSpeedTargetIndex);
153 }
154
StaticUpdate(const float timeStep)155 void PlayerShipController::StaticUpdate(const float timeStep)
156 {
157 vector3d v;
158 matrix4x4d m;
159
160 int mouseMotion[2];
161 // have to use this function. SDL mouse position event is bugged in windows
162 SDL_GetRelativeMouseState(mouseMotion + 0, mouseMotion + 1); // call to flush
163
164 if (m_ship->GetFlightState() == Ship::FLYING) {
165 switch (m_flightControlState) {
166 case CONTROL_FIXSPEED:
167 PollControls(timeStep, true, mouseMotion);
168 if (IsAnyLinearThrusterKeyDown()) break;
169 v = -m_ship->GetOrient().VectorZ() * m_setSpeed;
170 if (m_setSpeedTarget) {
171 v += m_setSpeedTarget->GetVelocityRelTo(m_ship->GetFrame());
172 }
173 m_ship->AIMatchVel(v);
174 break;
175 case CONTROL_FIXHEADING_FORWARD:
176 case CONTROL_FIXHEADING_BACKWARD:
177 case CONTROL_FIXHEADING_NORMAL:
178 case CONTROL_FIXHEADING_ANTINORMAL:
179 case CONTROL_FIXHEADING_RADIALLY_INWARD:
180 case CONTROL_FIXHEADING_RADIALLY_OUTWARD:
181 case CONTROL_FIXHEADING_KILLROT:
182 PollControls(timeStep, true, mouseMotion);
183 if (IsAnyAngularThrusterKeyDown()) break;
184 v = m_ship->GetVelocity().NormalizedSafe();
185 if (m_flightControlState == CONTROL_FIXHEADING_BACKWARD ||
186 m_flightControlState == CONTROL_FIXHEADING_ANTINORMAL)
187 v = -v;
188 if (m_flightControlState == CONTROL_FIXHEADING_NORMAL ||
189 m_flightControlState == CONTROL_FIXHEADING_ANTINORMAL)
190 v = v.Cross(m_ship->GetPosition().NormalizedSafe());
191 if (m_flightControlState == CONTROL_FIXHEADING_RADIALLY_INWARD)
192 v = -m_ship->GetPosition().NormalizedSafe();
193 if (m_flightControlState == CONTROL_FIXHEADING_RADIALLY_OUTWARD)
194 v = m_ship->GetPosition().NormalizedSafe();
195 if (m_flightControlState == CONTROL_FIXHEADING_KILLROT) {
196 v = -m_ship->GetOrient().VectorZ();
197 if (m_ship->GetAngVelocity().Length() < 0.0001) // fixme magic number
198 SetFlightControlState(CONTROL_MANUAL);
199 }
200
201 m_ship->AIFaceDirection(v);
202 break;
203 case CONTROL_MANUAL:
204 PollControls(timeStep, false, mouseMotion);
205 break;
206 case CONTROL_AUTOPILOT:
207 if (m_ship->AIIsActive()) break;
208 Pi::game->RequestTimeAccel(Game::TIMEACCEL_1X);
209 // AIMatchVel(vector3d(0.0)); // just in case autopilot doesn't...
210 // actually this breaks last timestep slightly in non-relative target cases
211 m_ship->AIMatchAngVelObjSpace(vector3d(0.0));
212 if (Frame::GetFrame(m_ship->GetFrame())->IsRotFrame())
213 SetFlightControlState(CONTROL_FIXSPEED);
214 else
215 SetFlightControlState(CONTROL_MANUAL);
216 m_setSpeed = 0.0;
217 break;
218 default: assert(0); break;
219 }
220 } else {
221 SetFlightControlState(CONTROL_MANUAL);
222
223 // TODO: this is a bit monkey-patched, but calling from SetFlightControlState doesn't properly clear the mouse capture state.
224 // Do it here so we properly react to becoming docked while holding the mouse button down
225 if (m_ship->GetFlightState() == Ship::DOCKED && m_mouseActive) {
226 Pi::input->SetCapturingMouse(false);
227 m_mouseActive = false;
228 }
229 }
230
231 //call autopilot AI, if active (also applies to set speed and heading lock modes)
232 OS::EnableFPE();
233 m_ship->AITimeStep(timeStep);
234 OS::DisableFPE();
235 }
236
CheckControlsLock()237 void PlayerShipController::CheckControlsLock()
238 {
239 m_controlsLocked = (Pi::game->IsPaused() || Pi::player->IsDead() || (m_ship->GetFlightState() != Ship::FLYING) || !InputBindings.active || (Pi::GetView() != Pi::game->GetWorldView())); //to prevent moving the ship in starmap etc.
240 }
241
GetMouseDir() const242 vector3d PlayerShipController::GetMouseDir() const
243 {
244 // translate from system to local frame
245 return m_mouseDir * Frame::GetFrame(m_ship->GetFrame())->GetOrient();
246 }
247
248 // needs to run inside CameraContext::Begin/EndFrame();
GetMouseViewDir() const249 vector3d PlayerShipController::GetMouseViewDir() const
250 {
251 // orientation according to mouse
252 matrix3x3d cam_rot = Pi::game->GetWorldView()->GetCameraContext()->GetCameraOrient();
253 vector3d mouseDir = GetMouseDir() * cam_rot;
254 if (m_invertMouse)
255 mouseDir = -mouseDir;
256 return (m_ship->GetPhysRadius() * 1.5) * mouseDir;
257 }
258
259 // mouse wraparound control function
clipmouse(double cur,double inp)260 static double clipmouse(double cur, double inp)
261 {
262 if (cur * cur > 0.7 && cur * inp > 0) return 0.0;
263 if (inp > 0.2) return 0.2;
264 if (inp < -0.2) return -0.2;
265 return inp;
266 }
267
PollControls(const float timeStep,const bool force_rotation_damping,int * mouseMotion)268 void PlayerShipController::PollControls(const float timeStep, const bool force_rotation_damping, int *mouseMotion)
269 {
270 static bool stickySpeedKey = false;
271 CheckControlsLock();
272 if (m_controlsLocked) return;
273
274 m_ship->ClearThrusterState();
275 m_ship->SetGunState(0, 0);
276 m_ship->SetGunState(1, 0);
277
278 // vector3d wantAngVel(0.0);
279 double angThrustSoftness = 10.0;
280
281 const float linearThrustPower = (InputBindings.thrustLowPower->IsActive() ? m_lowThrustPower : 1.0f);
282
283 if (Pi::input->MouseButtonState(SDL_BUTTON_RIGHT)) {
284 // use ship rotation relative to system, unchanged by frame transitions
285 matrix3x3d rot = m_ship->GetOrientRelTo(Frame::GetFrame(m_ship->GetFrame())->GetNonRotFrame());
286 if (!m_mouseActive && !m_disableMouseFacing) {
287 m_mouseDir = -rot.VectorZ();
288 m_mouseX = m_mouseY = 0;
289 m_mouseActive = true;
290 Pi::input->SetCapturingMouse(true);
291 }
292 vector3d objDir = m_mouseDir * rot;
293
294 const double radiansPerPixel = 0.00002 * m_fovY;
295 const int maxMotion = std::max(abs(mouseMotion[0]), abs(mouseMotion[1]));
296 const double accel = Clamp(maxMotion / 4.0, 0.0, 90.0 / m_fovY);
297
298 m_mouseX += mouseMotion[0] * accel * radiansPerPixel;
299 double modx = clipmouse(objDir.x, m_mouseX);
300 m_mouseX -= modx;
301
302 const bool invertY = (Pi::input->IsMouseYInvert() ? !m_invertMouse : m_invertMouse);
303
304 m_mouseY += mouseMotion[1] * accel * radiansPerPixel * (invertY ? -1 : 1);
305 double mody = clipmouse(objDir.y, m_mouseY);
306 m_mouseY -= mody;
307
308 if (!is_zero_general(modx) || !is_zero_general(mody)) {
309 matrix3x3d mrot = matrix3x3d::RotateY(modx) * matrix3x3d::RotateX(mody);
310 m_mouseDir = (rot * (mrot * objDir)).Normalized();
311 }
312 } else {
313 if (m_mouseActive)
314 Pi::input->SetCapturingMouse(false);
315
316 m_mouseActive = false;
317 }
318
319 if (m_flightControlState == CONTROL_FIXSPEED) {
320 double oldSpeed = m_setSpeed;
321 if (stickySpeedKey && !InputBindings.speedControl->IsActive())
322 stickySpeedKey = false;
323
324 if (!stickySpeedKey) {
325 const double MAX_SPEED = 300000000;
326 m_setSpeed += InputBindings.speedControl->GetValue() * std::max(std::abs(m_setSpeed) * 0.05, 1.0);
327 m_setSpeed = Clamp(m_setSpeed, -MAX_SPEED, MAX_SPEED);
328
329 if (((oldSpeed < 0.0) && (m_setSpeed >= 0.0)) ||
330 ((oldSpeed > 0.0) && (m_setSpeed <= 0.0))) {
331 // flipped from going forward to backwards. make the speed 'stick' at zero
332 // until the player lets go of the key and presses it again
333 stickySpeedKey = true;
334 m_setSpeed = 0;
335 }
336 }
337 }
338
339 if (InputBindings.thrustForward->IsActive())
340 m_ship->SetThrusterState(2, -linearThrustPower * InputBindings.thrustForward->GetValue());
341 if (InputBindings.thrustUp->IsActive())
342 m_ship->SetThrusterState(1, linearThrustPower * InputBindings.thrustUp->GetValue());
343 if (InputBindings.thrustLeft->IsActive())
344 m_ship->SetThrusterState(0, -linearThrustPower * InputBindings.thrustLeft->GetValue());
345
346 if (InputBindings.primaryFire->IsActive() || (Pi::input->MouseButtonState(SDL_BUTTON_LEFT) && Pi::input->MouseButtonState(SDL_BUTTON_RIGHT))) {
347 //XXX worldview? madness, ask from ship instead
348 m_ship->SetGunState(Pi::game->GetWorldView()->GetActiveWeapon(), 1);
349 }
350
351 vector3d wantAngVel = vector3d(
352 InputBindings.pitch->GetValue(),
353 InputBindings.yaw->GetValue(),
354 InputBindings.roll->GetValue());
355
356 if (InputBindings.killRot->IsActive()) SetFlightControlState(CONTROL_FIXHEADING_KILLROT);
357
358 if (InputBindings.thrustLowPower->IsActive())
359 angThrustSoftness = 50.0;
360
361 if (wantAngVel.Length() >= 0.001 || force_rotation_damping || m_rotationDamping) {
362 if (Pi::game->GetTimeAccel() != Game::TIMEACCEL_1X) {
363 for (int axis = 0; axis < 3; axis++)
364 wantAngVel[axis] = wantAngVel[axis] * Pi::game->GetInvTimeAccelRate();
365 }
366
367 m_ship->AIModelCoordsMatchAngVel(wantAngVel, angThrustSoftness);
368 }
369
370 if (m_mouseActive && !m_disableMouseFacing) m_ship->AIFaceDirection(GetMouseDir());
371 }
372
IsAnyAngularThrusterKeyDown()373 bool PlayerShipController::IsAnyAngularThrusterKeyDown()
374 {
375 return InputBindings.pitch->IsActive() || InputBindings.yaw->IsActive() || InputBindings.roll->IsActive();
376 }
377
IsAnyLinearThrusterKeyDown()378 bool PlayerShipController::IsAnyLinearThrusterKeyDown()
379 {
380 return InputBindings.thrustForward->IsActive() || InputBindings.thrustLeft->IsActive() || InputBindings.thrustUp->IsActive();
381 }
382
SetFlightControlState(FlightControlState s)383 void PlayerShipController::SetFlightControlState(FlightControlState s)
384 {
385 if (m_flightControlState == s)
386 return;
387
388 m_flightControlState = s;
389 m_ship->AIClearInstructions();
390 //set desired velocity to current actual
391 if (m_flightControlState == CONTROL_FIXSPEED) {
392 // Speed is set to the projection of the velocity onto the target.
393
394 vector3d shipVel = m_setSpeedTarget ?
395 // Ship's velocity with respect to the target, in current frame's coordinates
396 -m_setSpeedTarget->GetVelocityRelTo(m_ship) :
397 // Ship's velocity with respect to current frame
398 m_ship->GetVelocity();
399
400 // A change from Manual to Set Speed never sets a negative speed.
401 m_setSpeed = std::max(shipVel.Dot(-m_ship->GetOrient().VectorZ()), 0.0);
402 }
403
404 onChangeFlightControlState.emit();
405 }
406
SetLowThrustPower(float power)407 void PlayerShipController::SetLowThrustPower(float power)
408 {
409 assert((power >= 0.0f) && (power <= 1.0f));
410 m_lowThrustPower = power;
411 }
412
SetRotationDamping(bool enabled)413 void PlayerShipController::SetRotationDamping(bool enabled)
414 {
415 if (enabled != m_rotationDamping) {
416 m_rotationDamping = enabled;
417 onRotationDampingChanged.emit();
418 }
419 }
420
ToggleRotationDamping()421 void PlayerShipController::ToggleRotationDamping()
422 {
423 SetRotationDamping(!GetRotationDamping());
424 }
425
FireMissile()426 void PlayerShipController::FireMissile()
427 {
428 if (!Pi::player->GetCombatTarget())
429 return;
430 LuaObject<Ship>::CallMethod(Pi::player, "FireMissileAt", "any", static_cast<Ship *>(Pi::player->GetCombatTarget()));
431 }
432
ToggleSetSpeedMode()433 void PlayerShipController::ToggleSetSpeedMode()
434 {
435 if (m_ship->GetFlightState() != Ship::FLYING)
436 return;
437
438 if (GetFlightControlState() != CONTROL_FIXSPEED) {
439 SetFlightControlState(CONTROL_FIXSPEED);
440 } else {
441 SetFlightControlState(CONTROL_MANUAL);
442 }
443 }
444
GetCombatTarget() const445 Body *PlayerShipController::GetCombatTarget() const
446 {
447 return m_combatTarget;
448 }
449
GetNavTarget() const450 Body *PlayerShipController::GetNavTarget() const
451 {
452 return m_navTarget;
453 }
454
GetSetSpeedTarget() const455 Body *PlayerShipController::GetSetSpeedTarget() const
456 {
457 return m_setSpeedTarget;
458 }
459
SetCombatTarget(Body * const target,bool setSpeedTo)460 void PlayerShipController::SetCombatTarget(Body *const target, bool setSpeedTo)
461 {
462 if (setSpeedTo)
463 m_setSpeedTarget = target;
464 m_combatTarget = target;
465 onChangeTarget.emit();
466 }
467
SetNavTarget(Body * const target)468 void PlayerShipController::SetNavTarget(Body *const target)
469 {
470 m_navTarget = target;
471 onChangeTarget.emit();
472 }
473
SetSetSpeedTarget(Body * const target)474 void PlayerShipController::SetSetSpeedTarget(Body *const target)
475 {
476 m_setSpeedTarget = target;
477 // TODO: not sure, do we actually need this? we are only changing the set speed target
478 onChangeTarget.emit();
479 }
480