1 /**
2 	PlayerControl.c
3 	Functions to handle player controls (i.e. input keys).
4 
5 	@author Newton
6 */
7 
8 static const CON_Gamepad_Deadzone = 60;
9 static CON_VC_Players;
10 static g_player_cursor_pos; // array of [x,y] pos arrays; indexed by player. last cursor pos as sent by CON_CursorPos
11 
12 // PlayerControlRelease
13 // Called by engine whenever a control is issued
14 // Forwards control to special handler or cursor
15 // Return whether handled
16 // documented in /docs/sdk/script/fn
PlayerControl(int plr,int ctrl,id spec_id,int x,int y,int strength,bool repeat,int status)17 global func PlayerControl(int plr, int ctrl, id spec_id, int x, int y, int strength, bool repeat, int status)
18 {
19 	var release = status == CONS_Up;
20 	//Log("%d, %s, %i, %d, %d, %d, %v, %v", plr, GetPlayerControlName(ctrl), spec_id, x,y,strength, repeat, status);
21 	// Control handled by definition? Forward
22 	if (spec_id) return spec_id->ForwardedPlayerControl(plr, ctrl, x, y, strength, repeat, status);
23 
24 	// Forward control to player
25 	if (Control2Player(plr,ctrl, x, y, strength, repeat, status)) return true;
26 
27 	// Forward control to cursor
28 	var cursor = GetCursor(plr);
29 	if (cursor && cursor->GetCrewEnabled())
30 	{
31 		// Object controlled by plr
32 		cursor->SetController(plr);
33 
34 		// menu controls:
35 
36 		if (cursor->~GetMenu() && status != CONS_Moved)
37 		{
38 			// direction keys are always forwarded to the menu
39 			// (because the clonk shall not move while the menu is open)
40 			var handled = true;
41 			if (ctrl == CON_Left)       cursor->GetMenu()->~ControlLeft(release);
42 			else if (ctrl == CON_Right) cursor->GetMenu()->~ControlRight(release);
43 			else if (ctrl == CON_Up)    cursor->GetMenu()->~ControlUp(release);
44 			else if (ctrl == CON_Down)  cursor->GetMenu()->~ControlDown(release);
45 			else handled = false;
46 			if (handled) return true;
47 
48 			// cancel menu
49 			if (ctrl == CON_CancelMenu)
50 			{
51 				cursor->TryCancelMenu();
52 				return true;
53 			}
54 
55 			if (ctrl == CON_GUIClick1 || ctrl == CON_GUIClick2 || ctrl == CON_GUICursor)
56 			{
57 				var menux = cursor->GetMenu()->~GetX();
58 				var menuy = cursor->GetMenu()->~GetY();
59 
60 				var dx = x-menux;
61 				var dy = y-menuy;
62 
63 				if (ctrl == CON_GUICursor)
64 				{
65 					cursor->GetMenu()->~UpdateCursor(dx,dy);
66 					return true;
67 				}
68 				else if (release == true)
69 				{
70 					cursor->GetMenu()->~OnMouseClick(dx,dy,ctrl == CON_GUIClick2);
71 					return false;
72 				}
73 			}
74 		}
75 
76 		// local coordinates
77 		var cursorX = x, cursorY = y;
78 		if (x != nil || y != nil)
79 		{
80 			cursorX -= cursor->GetX();
81 			cursorY -= cursor->GetY();
82 		}
83 
84 		// Overload by effect?
85 		if (cursor->Control2Effect(plr, ctrl, cursorX, cursorY, strength, repeat, status)) return true;
86 
87 		if (cursor->ObjectControl(plr, ctrl, cursorX, cursorY, strength, repeat, status))
88 		{
89 			if (cursor && status == CONS_Down && !repeat)
90 			{
91 				// non-mouse controls reset view
92 				if (!x && !y) ResetCursorView(plr);
93 			}
94 			return true;
95 		}
96 		//else Log("-- not handled");
97 
98 	}
99 
100 	// Nothing to handle control then
101 	return false;
102 }
103 
InitializePlayerControl(int plr,string controlset_name,bool keyboard,bool mouse,bool gamepad)104 global func InitializePlayerControl(int plr, string controlset_name, bool keyboard, bool mouse, bool gamepad)
105 {
106 	// VC = VirtualCursor
107 	if(!CON_VC_Players)
108 		CON_VC_Players = CreateArray();
109 
110 	CON_VC_Players[plr] = !mouse;
111 
112 	// for all clonks...
113 	for (var clonk in FindObjects(Find_OCF(OCF_CrewMember)))
114 	{
115 		clonk->~ReinitializeControls();
116 	}
117 }
118 
PlayerHasVirtualCursor(int plr)119 global func PlayerHasVirtualCursor(int plr)
120 {
121 	if (!CON_VC_Players)
122 		return false;
123 
124 	return CON_VC_Players[plr];
125 }
126 
127 // Control2Player
128 // Player-wide controls
Control2Player(int plr,int ctrl,int x,int y,int strength,bool repeat,int status)129 global func Control2Player(int plr, int ctrl, int x, int y, int strength, bool repeat, int status)
130 {
131 	// select previous or next
132 	if (ctrl == CON_PreviousCrew)
133 		return ShiftCursor(plr, true);
134 	if (ctrl == CON_NextCrew)
135 		return ShiftCursor(plr, false);
136 
137 	// all those hotkeys...
138 	var hotkey = 0;
139 	if (ctrl == CON_PlayerHotkey0) hotkey = 10;
140 	if (ctrl == CON_PlayerHotkey1) hotkey = 1;
141 	if (ctrl == CON_PlayerHotkey2) hotkey = 2;
142 	if (ctrl == CON_PlayerHotkey3) hotkey = 3;
143 	if (ctrl == CON_PlayerHotkey4) hotkey = 4;
144 	if (ctrl == CON_PlayerHotkey5) hotkey = 5;
145 	if (ctrl == CON_PlayerHotkey6) hotkey = 6;
146 	if (ctrl == CON_PlayerHotkey7) hotkey = 7;
147 	if (ctrl == CON_PlayerHotkey8) hotkey = 8;
148 	if (ctrl == CON_PlayerHotkey9) hotkey = 9;
149 
150 	if (hotkey > 0)
151 	{
152 		// valid crew number?
153 		var crew = GetCrew(plr,hotkey-1);
154 		if (!crew) return false;
155 		// stop previously selected crew
156 		StopSelected();
157 
158 		// set cursor if not disabled etc.
159 		return SetCursor(plr, crew);
160 	}
161 
162 	// Modifier keys - do not handle the key. The GetPlayerControlState will still return the correct value when the key is held down.
163 	if (ctrl == CON_ModifierMenu1) return false;
164 
165 	// cursor pos info - store in player values
166 	if (ctrl == CON_CursorPos)
167 	{
168 		if (!g_player_cursor_pos) g_player_cursor_pos = CreateArray(plr+1);
169 		g_player_cursor_pos[plr] = [x, y];
170 		return true;
171 	}
172 	/*
173 	if (ctrl == CON_Test)
174 	{
175 		Message(Format("%d %d (%d %d) %d [%d %d]", plr, ctrl, x, y, strength, repeat, release));
176 		return true;
177 	}
178 	*/
179 	return false;
180 }
181 
182 /* return info of last sent CON_CursorPos packet for that player as [x, y] */
GetPlayerCursorPos(int plr)183 global func GetPlayerCursorPos(int plr)
184 {
185 	if (!g_player_cursor_pos) return 0;
186 	return g_player_cursor_pos[plr];
187 }
188 
StopSelected(int plr)189 global func StopSelected(int plr)
190 {
191 	var cursor = GetCursor(plr);
192 	if (cursor)
193 	{
194 		cursor->SetCommand("None");
195 		cursor->SetComDir(COMD_Stop);
196 	}
197 }
198 
199 /* Object functions */
200 // To be called in an object context only!
201 
202 // Control2Effect
203 // Call control function in all effects that have "Control" in their name
Control2Effect(int plr,int ctrl,int x,int y,int strength,bool repeat,int status)204 global func Control2Effect(int plr, int ctrl, int x, int y, int strength, bool repeat, int status)
205 {
206 	// x and y are local coordinates
207 	if (!this) return false;
208 
209 	// Count down from EffectCount, in case effects get deleted
210 	var i = GetEffectCount("*Control*", this), fx;
211 	while (i--)
212 	{
213 		fx = GetEffect("*Control*", this, i);
214 		if (fx)
215 			if (EffectCall(this, fx, "Control", ctrl, x,y,strength, repeat, status))
216 				return true;
217 	}
218 	// No effect handled the control
219 	return false;
220 }
221 
222 // ObjectControl
223 // Called from PlayerControl when a control is issued to the cursor
224 // Return whether handled
225 // To be overloaded by specific objects to enable additional controls
ObjectControl(int plr,int ctrl,int x,int y,int strength,bool repeat,int status)226 global func ObjectControl(int plr, int ctrl, int x, int y, int strength, bool repeat, int status)
227 {
228 	if (!this) return false;
229 
230 	// Any control resets a previously given command
231 	SetCommand("None");
232 
233 	// Movement controls
234 	if (ctrl == CON_Left || ctrl == CON_Right || ctrl == CON_Up || ctrl == CON_Down || ctrl == CON_Jump)
235 		return ObjectControlMovement(plr, ctrl, strength, status, repeat);
236 
237 	// Unhandled control
238 	return false;
239 }
240 
241 // Find an object with an entrance in front of this object whose entrance is at
242 // the right position
GetEntranceObject()243 global func GetEntranceObject()
244 {
245 	if (!this) return nil;
246 
247 	// object with an entrance on target position
248 	var obj = FindObject(Find_OCF(OCF_Entrance), Find_Layer(GetObjectLayer()),
249 	                     Find_AtPoint(0,0), Find_Exclude(this));
250 	if (!obj) return nil;
251 
252 	var x = obj->GetDefCoreVal("Entrance","DefCore",0) + obj->GetX();
253 	var y = obj->GetDefCoreVal("Entrance","DefCore",1) + obj->GetY();
254 	var wdt = obj->GetDefCoreVal("Entrance","DefCore",2);
255 	var hgt = obj->GetDefCoreVal("Entrance","DefCore",3);
256 
257 	// entrance is on the vehicle?
258 	if (!Inside(GetX(), x, x+wdt)) return nil;
259 	if (!Inside(GetY(), y, y+hgt)) return nil;
260 
261 	return obj;
262 }
NameComDir(comdir)263 global func NameComDir(comdir)
264 {
265 	if(comdir == COMD_Stop) return "COMD_Stop";
266 	if(comdir == COMD_Up) return "COMD_Up";
267 	if(comdir == COMD_UpRight) return "COMD_UpRight";
268 	if(comdir == COMD_UpLeft) return "COMD_UpLeft";
269 	if(comdir == COMD_Right) return "COMD_Right";
270 	if(comdir == COMD_Left) return "COMD_Left";
271 	if(comdir == COMD_Down) return "COMD_Down";
272 	if(comdir == COMD_DownRight) return "COMD_DownRight";
273 	if(comdir == COMD_DownLeft) return "COMD_DownLeft";
274 	if(comdir == COMD_None) return "COMD_None";
275 }
276 // Called when CON_Left/Right/Up/Down controls are issued/released
277 // Return whether handled
ObjectControlMovement(int plr,int ctrl,int strength,int status,bool repeat)278 global func ObjectControlMovement(int plr, int ctrl, int strength, int status, bool repeat)
279 {
280 	if (!this) return false;
281 
282 	// movement is only possible when not contained
283 	if (Contained()) return false;
284 
285 	// this is for controlling movement with Analogpad
286 	if (status == CONS_Down)
287 		if (strength != nil && strength < CON_Gamepad_Deadzone)
288 			return true;
289 
290 	var proc = GetProcedure();
291 	// Some specific movement controls
292 	if (status == CONS_Down)
293 	{
294 		// Jump control
295 		if (ctrl == CON_Jump)
296 		{
297 			if (proc == "WALK" && GetComDir() == COMD_Up)
298 				SetComDir(COMD_Stop);
299 			if (proc == "WALK")
300 			{
301 				this->ObjectCommand("Jump");
302 				return true;
303 			}
304 		}
305 		if (proc == "SWIM" && !GBackSemiSolid(0,-5)) //Water jump
306 		{
307 			if (ctrl == CON_Up) return false;
308 			else if(ctrl == CON_Jump) this->ObjectCommand("Jump");
309 		}
310 		if (proc == "SCALE") // Let go from scaling a wall
311 		{
312 			//Wall kick
313 			if (ctrl == CON_Left && GetDir() == DIR_Right && GetComDir() == COMD_Up)
314 				return this->ObjectCommand("Jump");
315 			if (ctrl == CON_Right && GetDir() == DIR_Left && GetComDir() == COMD_Up)
316 				return this->ObjectCommand("Jump");
317 
318 			//Let go of wall
319 			if (ctrl == CON_Left && GetDir() == DIR_Right) return this->ObjectComLetGo(-10);
320 			if (ctrl == CON_Right && GetDir() == DIR_Left) return this->ObjectComLetGo(+10);
321 		}
322 		else if (proc == "HANGLE") // Let go from hangling the ceiling
323 		{
324 			if (ctrl == CON_Down) return this->ObjectComLetGo(0,0);
325 		}
326 		// Direct turnaround if object is standing still. Valid for any procedure in OC
327 		if (!GetXDir())
328 		{
329 			if (ctrl == CON_Left) SetDir(DIR_Left);
330 			else if (ctrl == CON_Right) SetDir(DIR_Right);
331 		}
332 	}
333 	else // release
334 	{
335 		// If rolling, allow to instantly switch to walking again.
336 		if (GetAction() == "Roll")
337 		{
338 			if (ctrl == CON_Left && GetDir() == DIR_Left || ctrl == CON_Right && GetDir() == DIR_Right)
339 				SetAction("Walk");
340 		}
341 	}
342 	return ObjectControlUpdateComdir(plr);
343 }
344 
345 // Updates ComDir of object based on current Con_*-directional controls
346 // Return whether actual, effective direction of movement changed
ObjectControlUpdateComdir(int plr)347 global func ObjectControlUpdateComdir(int plr)
348 {
349 	if (!this) return false;
350 
351 	// Generic movement: Update ComDir based on current control state
352 	var new_comdir = GetPlayerConDir(plr, CON_Left, CON_Up, CON_Right, CON_Down);
353 	var old_comdir = GetComDir();
354 	if (new_comdir != old_comdir)
355 	{
356 		// ComDir changed. Update.
357 		SetComDir(new_comdir);
358 		//var s = "";
359 		//if (GetPlayerControlState(plr, CON_Left))	s = Format("%sL", s);
360 		//if (GetPlayerControlState(plr, CON_Up))		s = Format("%sU", s);
361 		//if (GetPlayerControlState(plr, CON_Right)) s = Format("%sR", s);
362 		//if (GetPlayerControlState(plr, CON_Down))	s = Format("%sD", s);
363 		//s = Format("%s %s", s, ["Stop", "Up", "UpRight", "Right", "DownRight", "Down", "DownLeft", "Left", "UpLeft"][new_comdir]);
364 		//Message("@%s", this, s);
365 		// The control is only handled if it had an actual effect on the current movement direction of the Clonk
366 		var old = ComDir2XY(old_comdir);
367 		var new = ComDir2XY(new_comdir);
368 		var old_cx = old[0], old_cy = old[1], new_cx = new[0], new_cy = new[1];
369 		var is_handled;
370 		var proc = GetProcedure();
371 		if (proc == "WALK" || proc == "HANGLE" || proc == "PUSH" || proc == "PULL" || proc == "FLIGHT")
372 			// Only horizontal movement changed actual direction
373 			// Also, enforce clear Left/Right commands without leftover Up/Down
374 			// CON_Down is never handled this way, thus forcing a CON_Stop
375 			is_handled = (old_cx != new_cx) && !new_cy;
376 		else if (proc == "SCALE")
377 			// Only vertical movement changed actual direction
378 			// Also, enfore clear Up/Down to prevent "Zuppel" in corner
379 			is_handled = (old_cy != new_cy) && !new_cx;
380 		else if (proc == "SWIM" || proc == "FLOAT" || proc == "DIG")
381 			is_handled = (old_cx != new_cx || old_cy != new_cy); // Free 360 degree movement
382 		else
383 			is_handled = false;
384 		return is_handled;
385 	}
386 	else
387 	{
388 		// ComDir did not change. -> The control was not handled
389 		//Log("NoChange");
390 		return false;
391 	}
392 }
393 
394 // selects the next/previous crew member (that is not disabled)
ShiftCursor(int plr,bool back,bool force)395 global func ShiftCursor(int plr, bool back, bool force)
396 {
397 	// Is the selected Clonk busy at the moment? E.g. uncloseable menu open..
398 	if (!force)
399 	{
400 		var cursor = GetCursor(plr);
401 		if (cursor && cursor->~RejectShiftCursor()) return false;
402 	}
403 
404 	// get index of currently selected crew
405 	var index = 0;
406 	while (index < GetCrewCount(plr))
407 	{
408 		if (GetCursor(plr) == GetCrew(plr,index)) break;
409 		index++;
410 	}
411 
412 	// a short explanation here:
413 	// when shifting the cursor to the next crew member, this crew member
414 	// might be disabled via SetCrewEnabled. That is why we have to go to
415 	// the crew after the next and perhaps even the crew after the next
416 	// after the next. Also, we need to stop this skipping after all crew
417 	// members have been checked in the special case that all crew members
418 	// are disabled
419 	var maxcycle = GetCrewCount(plr);
420 	var cycle = 0;
421 
422 	do {
423 		if (back)
424 		{
425 			--index;
426 			if (index < 0) index = GetCrewCount(plr)-1;
427 		}
428 		else
429 		{
430 			++index;
431 			if (index >= GetCrewCount(plr)) index = 0;
432 		}
433 		++cycle;
434 	} while (cycle < maxcycle && !(GetCrew(plr,index)->GetCrewEnabled()));
435 
436 	// Changing the cursor closes all menus that are associated with the old cursor.
437 	// However, if a menu is not closable, then it requires the attention of the player and switching the cursor is disabled..
438 	var current_cursor = GetCursor(plr);
439 	var new_cursor = GetCrew(plr, index);
440 	if (current_cursor == new_cursor) return false;
441 
442 	StopSelected(plr);
443 	if (current_cursor)
444 		current_cursor->~OnShiftCursor(new_cursor);
445 
446 	return SetCursor(plr, new_cursor);
447 }
448 
449 // Temporarily used for Debugging!
450 // Helper function to turn CON_*-constants into strings
GetPlayerControlName(int ctrl)451 global func GetPlayerControlName(int ctrl)
452 {
453 	var con_name = GetConstantNameByValue(ctrl, "CON_");
454 	if (!con_name) con_name = Format("Unknown(%d)", ctrl);
455 	return con_name;
456 }
457 
458 // Return COMD_*-constant corresponding to current state of passed directional controls
GetPlayerConDir(int plr,int con_left,int con_up,int con_right,int con_down)459 global func GetPlayerConDir(int plr, int con_left, int con_up, int con_right, int con_down)
460 {
461 	var x,y;
462 	if (GetPlayerControlState(plr, con_left))	--x;
463 	if (GetPlayerControlState(plr, con_up))		--y;
464 	if (GetPlayerControlState(plr, con_right)) ++x;
465 	if (GetPlayerControlState(plr, con_down))	++y;
466 	// Creating an array here for every keypress/release
467 	// Would be so cool to have this static const. Guenther?
468 	var dir_coms = [COMD_UpLeft, COMD_Up, COMD_UpRight, COMD_Left, COMD_Stop, COMD_Right, COMD_DownLeft, COMD_Down, COMD_DownRight];
469 	return dir_coms[y*3+x+4];
470 }
471 
472 // Returns coordinate directions associated with a COMD_Constant
ComDir2XY(int comd)473 global func ComDir2XY(int comd)
474 {
475 	// Creating an array here for every keypress/release
476 	// Would be so cool to have this static const. Guenther?
477 	return [[0,0,1,1,1,0,-1,-1,-1][comd], [0,-1,-1,0,1,1,1,0,-1][comd]];
478 }
479 
ObjectCommand(string command,object target,int tx,int ty,object target2,data)480 global func ObjectCommand(string command, object target, int tx, int ty, object target2, /*any*/ data)
481 {
482 	// this function exists to be overloadable by ClonkControl.c4d
483 	if (!this)
484 		return;
485 	this->SetCommand(command, target, tx, ty, target2, data);
486 }
487 
488 // Let go from scaling or hangling
ObjectComLetGo(int vx,int vy)489 global func ObjectComLetGo(int vx, int vy)
490 {
491 	if (!SetAction("Jump")) return false;
492 	SetXDir(vx); SetYDir(vy);
493 	return true;
494 }
495 
496 /* Mouse Hovering */
497 
498 // Engine callback when the mouse hovers over an object (entering) or stops hovering over an object (leaving).
499 // Either leaving, entering or both are set. They can be nil, but never both at the same time.
500 // dragged is an object being drag(&not dropped yet)
MouseHover(int player,object leaving,object entering,object dragged)501 global func MouseHover(int player, object leaving, object entering, object dragged)
502 {
503 	// Leaving the hovering zone should be processed first.
504 	if (leaving)
505 		leaving->~OnMouseOut(player, dragged);
506 	// Then process entering a new hovering zone.
507 	if (entering)
508 		entering->~OnMouseOver(player, dragged);
509 	return true;
510 }
511 
512 /* Drag & Drop */
513 
514 // Engine callback on drag&drop: gives the player, the dragged obect and the object which is being dropped(can be nil).
MouseDragDrop(int plr,object source,object target)515 global func MouseDragDrop(int plr, object source, object target)
516 {
517 	//Log("MouseDragDrop(%d, %s, %s)", plr, source->GetName(), target->GetName());
518 	if (!source) return false; // can happen if source got deleted after control was queued
519 	var src_drag = source->~OnMouseDrag(plr);
520 	if (!src_drag)
521 		return false;
522 
523 	// If there is drop target, check whether it accepts the drop.
524 	if (target)
525 		if (!target->~OnMouseDrop(plr, src_drag))
526 			return false;
527 
528 	// Notify the drop object it succeeded.
529 	if (source)
530 		source->~OnMouseDragDone(src_drag, target);
531 	return true;
532 }
533 
534 /* Debug */
535 
536 // uncomment this to get log messages for any player control issued
537 /*global func PlayerControl(int plr, int ctrl, id spec_id, int x, int y, int strength, bool repeat, bool release)
538 {
539 	var r = inherited(plr, ctrl, spec_id, x, y, strength, repeat, release, ...), rs;
540 	if (r) rs = ""; else rs = "!";
541 	Log("%s%d, %s, %i, %d, %d, %d, %v, %v", rs, plr, GetPlayerControlName(ctrl), spec_id, x,y,strength, repeat, release);
542 	return r;
543 }*/
544 
545 /*
546 This is used by Library_ClonkInventoryControl and needs to be a global function (in a non-appendto).
547 This function returns the priority of an object when selecting an object to pick up.
548 */
Library_ClonkInventoryControl_Sort_Priority(int x_position)549 global func Library_ClonkInventoryControl_Sort_Priority(int x_position)
550 {
551 	// Objects are sorted by position, preferring the direction of the key press.
552 	var priority_x = GetX() - x_position;
553 	if (priority_x < 0) priority_x += 1000;
554 	return priority_x;
555 }
556