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