summaryrefslogtreecommitdiff
path: root/sourcemod/scripting/include/gokz.inc
blob: edbd89696eb8a816afc570387de61eb89609bdd6 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
/*
	GOKZ General Include

	Website: https://bitbucket.org/kztimerglobalteam/gokz
*/

#if defined _gokz_included_
#endinput
#endif
#define _gokz_included_
#include <cstrike>
#include <movement>

#include <gokz/version>



// =====[ ENUMS ]=====

enum ObsMode
{
	ObsMode_None = 0,  // Not in spectator mode
	ObsMode_DeathCam,  // Special mode for death cam animation
	ObsMode_FreezeCam,  // Zooms to a target, and freeze-frames on them
	ObsMode_Fixed,  // View from a fixed camera position
	ObsMode_InEye,  // Follow a player in first person view
	ObsMode_Chase,  // Follow a player in third person view
	ObsMode_Roaming // Free roaming
};



// =====[ CONSTANTS ]=====

#define GOKZ_SOURCE_URL "https://github.com/KZGlobalTeam/gokz"
#define GOKZ_UPDATER_BASE_URL "http://updater.gokz.org/v2/"
#define GOKZ_COLLISION_GROUP_STANDARD 2
#define GOKZ_COLLISION_GROUP_NOTRIGGER 1
#define GOKZ_TP_FREEZE_TICKS 5
#define EPSILON 0.000001
#define PI 3.14159265359
#define SPEED_NORMAL 250.0
#define SPEED_NO_WEAPON 260.0
#define FLOAT_MAX view_as<float>(0x7F7FFFFF)
#define SF_BUTTON_USE_ACTIVATES 1024
#define IGNORE_JUMP_TIME 0.2
stock float PLAYER_MINS[3] = {-16.0, -16.0, 0.0};
stock float PLAYER_MAXS[3] = {16.0, 16.0, 72.0};
stock float PLAYER_MAXS_DUCKED[3] = {16.0, 16.0, 54.0};



// =====[ STOCKS ]=====

/**
 * Represents a time float as a string e.g. 01:23.45.
 *
 * @param time			Time in seconds.
 * @param precise		Whether to include fractional seconds.
 * @return				String representation of time.
 */
stock char[] GOKZ_FormatTime(float time, bool precise = true)
{
	char formattedTime[12];

	int roundedTime = RoundFloat(time * 100); // Time rounded to number of centiseconds

	int centiseconds = roundedTime % 100;
	roundedTime = (roundedTime - centiseconds) / 100;
	int seconds = roundedTime % 60;
	roundedTime = (roundedTime - seconds) / 60;
	int minutes = roundedTime % 60;
	roundedTime = (roundedTime - minutes) / 60;
	int hours = roundedTime;

	if (hours == 0)
	{
		if (precise)
		{
			FormatEx(formattedTime, sizeof(formattedTime), "%02d:%02d.%02d", minutes, seconds, centiseconds);
		}
		else
		{
			FormatEx(formattedTime, sizeof(formattedTime), "%d:%02d", minutes, seconds);
		}
	}
	else
	{
		if (precise)
		{
			FormatEx(formattedTime, sizeof(formattedTime), "%d:%02d:%02d.%02d", hours, minutes, seconds, centiseconds);
		}
		else
		{
			FormatEx(formattedTime, sizeof(formattedTime), "%d:%02d:%02d", hours, minutes, seconds);
		}
	}
	return formattedTime;
}

/**
 * Checks if the value is a valid client entity index, if they are in-game and not GOTV.
 *
 * @param client		Client index.
 * @return				Whether client is valid.
 */
stock bool IsValidClient(int client)
{
	return client >= 1 && client <= MaxClients && IsClientInGame(client) && !IsClientSourceTV(client);
}

/**
 * Returns the greater of two float values.
 *
 * @param value1		First value.
 * @param value2		Second value.
 * @return				Greatest value.
 */
stock float FloatMax(float value1, float value2)
{
	if (value1 >= value2)
	{
		return value1;
	}
	return value2;
}

/**
 * Returns the lesser of two float values.
 *
 * @param value1		First value.
 * @param value2		Second value.
 * @return				Lesser value.
 */
stock float FloatMin(float value1, float value2)
{
	if (value1 <= value2)
	{
		return value1;
	}
	return value2;
}

/**
 * Clamp a float value between an upper and lower bound.
 *
 * @param value			Preferred value.
 * @param min			Minimum value.
 * @param max			Maximum value.
 * @return				The closest value to the preferred value.
 */
stock float FloatClamp(float value, float min, float max)
{
	if (value >= max)
	{
		return max;
	}
	if (value <= min)
	{
		return min;
	}
	return value;
}


/**
 * Returns the greater of two int values.
 *
 * @param value1		First value.
 * @param value2		Second value.
 * @return				Greatest value.
 */
stock int IntMax(int value1, int value2)
{
	if (value1 >= value2)
	{
		return value1;
	}
	return value2;
}

/**
 * Returns the lesser of two int values.
 *
 * @param value1		First value.
 * @param value2		Second value.
 * @return				Lesser value.
 */
stock int IntMin(int value1, int value2)
{
	if (value1 <= value2)
	{
		return value1;
	}
	return value2;
}

/**
 * Rounds a float to the nearest specified power of 10.
 *
 * @param value			Value to round.
 * @param power			Power of 10 to round to.
 * @return				Rounded value.
 */
stock float RoundToPowerOfTen(float value, int power)
{
	float pow = Pow(10.0, float(power));
	return RoundFloat(value / pow) * pow;
}

/**
 * Sets all characters in a string to lower case.
 *
 * @param input			Input string.
 * @param output		Output buffer.
 * @param size			Maximum size of output.
 */
stock void String_ToLower(const char[] input, char[] output, int size)
{
	size--;
	int i = 0;
	while (input[i] != '\0' && i < size)
	{
		output[i] = CharToLower(input[i]);
		i++;
	}
	output[i] = '\0';
}

/**
 * Gets the client's observer mode.
 *
 * @param client		Client index.
 * @return				Current observer mode.
 */
stock ObsMode GetObserverMode(int client)
{
	return view_as<ObsMode>(GetEntProp(client, Prop_Send, "m_iObserverMode"));
}

/**
 * Gets the player a client is spectating.
 *
 * @param client		Client index.
 * @return				Client index of target, or -1 if not spectating anyone.
 */
stock int GetObserverTarget(int client)
{
	if (!IsValidClient(client))
	{
		return -1;
	}
	ObsMode mode = GetObserverMode(client);
	if (mode == ObsMode_InEye || mode == ObsMode_Chase)
	{
		return GetEntPropEnt(client, Prop_Send, "m_hObserverTarget");
	}
	return -1;
}

/**
 * Emits a sound to other players that are spectating the client.
 *
 * @param client		Client being spectated.
 * @param sound			Sound to play.
 */
stock void EmitSoundToClientSpectators(int client, const char[] sound)
{
	for (int i = 1; i <= MaxClients; i++)
	{
		if (IsValidClient(i) && GetObserverTarget(i) == client)
		{
			EmitSoundToClient(i, sound);
		}
	}
}

/**
 * Calculates the lowest angle from angle A to angle B.
 * Input and result angles are between -180 and 180.
 *
 * @param angleA		Angle A.
 * @param angleB		Angle B.
 * @return				Delta angle.
 */
stock float CalcDeltaAngle(float angleA, float angleB)
{
	float difference = angleB - angleA;

	if (difference > 180.0)
	{
		difference = difference - 360.0;
	}
	else if (difference <= -180.0)
	{
		difference = difference + 360.0;
	}

	return difference;
}

/**
 * Strips all color control characters in a string.
 * The Output buffer can be the same as the input buffer.
 * Original code by Psychonic, thanks.
 * Source: smlib
 *
 * @param input				Input String.
 * @param output			Output String.
 * @param size				Max Size of the Output string
 */
stock void Color_StripFromChatText(const char[] input, char[] output, int size)
{
	int x = 0;
	for (int i = 0; input[i] != '\0'; i++) {

		if (x + 1 == size)
		{
			break;
		}

		int character = input[i];

		if (character > 0x08)
		{
			output[x++] = character;
		}
	}

	output[x] = '\0';
}

/**
 * Returns an integer as a string.
 *
 * @param num				Integer to stringify.
 * @return					Integer as a string.
 */
stock char[] IntToStringEx(int num)
{
	char string[12];
	IntToString(num, string, sizeof(string));
	return string;
}

/**
 * Returns a float as a string.
 *
 * @param num				Float to stringify.
 * @return					Float as a string.
 */
stock char[] FloatToStringEx(float num)
{
	char string[32];
	FloatToString(num, string, sizeof(string));
	return string;
}

/**
 * Increment an index, looping back to 0 if the max value is reached.
 *
 * @param index			Current index.
 * @param buffer		Max value of index.
 * @return				Current index incremented, or 0 if max value is reached.
 */
stock int NextIndex(int index, int max)
{
	index++;
	if (index == max)
	{
		return 0;
	}
	return index;
}

/**
 * Reorders an array with current index at the front, and previous
 * values after, including looping back to the end after reaching
 * the start of the array.
 *
 * @param input			Array to reorder.
 * @param inputSize		Size of input array.
 * @param buffer		Output buffer.
 * @param bufferSize	Size of buffer.
 * @param index			Index of current/most recent value of input array.
 */
stock void SortByRecent(const int[] input, int inputSize, int[] buffer, int bufferSize, int index)
{
	int reorderedIndex = 0;
	for (int i = index; reorderedIndex < bufferSize && i >= 0; i--)
	{
		buffer[reorderedIndex] = input[i];
		reorderedIndex++;
	}
	for (int i = inputSize - 1; reorderedIndex < bufferSize && i > index; i--)
	{
		buffer[reorderedIndex] = input[i];
		reorderedIndex++;
	}
}

/**
 * Returns the Steam account ID for a given SteamID2.
 * Checks for invalid input are not very extensive.
 *
 * @param steamID2		SteamID2 to convert.
 * @return				Steam account ID, or -1 if invalid.
 */
stock int Steam2ToSteamAccountID(const char[] steamID2)
{
	char pieces[3][16];
	if (ExplodeString(steamID2, ":", pieces, sizeof(pieces), sizeof(pieces[])) != 3)
	{
		return -1;
	}

	int IDNumberPart1 = StringToInt(pieces[1]);
	int IDNumberPart2 = StringToInt(pieces[2]);
	if (pieces[1][0] != '0' && IDNumberPart1 == 0 || IDNumberPart1 != 0 && IDNumberPart1 != 1 || IDNumberPart2 <= 0)
	{
		return -1;
	}

	return IDNumberPart1 + (IDNumberPart2 << 1);
}

/**
 * Teleports a player and removes their velocity and base velocity
 * immediately and also every tick for the next 5 ticks. Automatically
 * makes the player crouch if there is a ceiling above them.
 *
 * @param client		Client index.
 * @param origin		Origin to teleport to.
 * @param angles		Eye angles to set.
 */
stock void TeleportPlayer(int client, const float origin[3], const float angles[3], bool setAngles = true, bool holdStill = true)
{
	// Clear the player's parent before teleporting to fix being
	// teleported into seemingly random places if the player has a parent.
	AcceptEntityInput(client, "ClearParent");

	Movement_SetOrigin(client, origin);
	Movement_SetVelocity(client, view_as<float>( { 0.0, 0.0, 0.0 } ));
	Movement_SetBaseVelocity(client, view_as<float>( { 0.0, 0.0, 0.0 } ));
	if (setAngles)
	{
		// NOTE: changing angles with TeleportEntity can fail due to packet loss!!!
		// (Movement_SetEyeAngles is a thin wrapper of TeleportEntity)
		Movement_SetEyeAngles(client, angles);
	}
	// Duck the player if there is something blocking them from above
	Handle trace = TR_TraceHullFilterEx(origin,
		origin,
		view_as<float>( { -16.0, -16.0, 0.0 } ),  // Standing players are 32 x 32 x 72
		view_as<float>( { 16.0, 16.0, 72.0 } ),
		MASK_PLAYERSOLID,
		TraceEntityFilterPlayers,
		client);
	bool ducked = TR_DidHit(trace);

	if (holdStill)
	{
		// Prevent noclip exploit
		SetEntProp(client, Prop_Send, "m_CollisionGroup", GOKZ_COLLISION_GROUP_STANDARD);

		// Intelligently hold player still to prevent booster and trigger exploits
		StartHoldStill(client, ducked);
	}
	else if (ducked)
	{
		ForcePlayerDuck(client);
	}

	delete trace;
}

static void StartHoldStill(int client, bool ducked)
{
	DataPack data = new DataPack();
	data.WriteCell(GetClientUserId(client));
	data.WriteCell(0); // tick counter
	data.WriteCell(GOKZ_TP_FREEZE_TICKS); // number of ticks to hold still
	data.WriteCell(ducked);
	ContinueHoldStill(data);
}

public void ContinueHoldStill(DataPack data)
{
	data.Reset();
	int client = GetClientOfUserId(data.ReadCell());
	int ticks = data.ReadCell();
	int tickCount = data.ReadCell();
	bool ducked = data.ReadCell();
	delete data;

	if (!IsValidClient(client))
	{
		return;
	}

	if (ticks < tickCount)
	{
		Movement_SetVelocity(client, view_as<float>( { 0.0, 0.0, 0.0 } ));
		Movement_SetBaseVelocity(client, view_as<float>( { 0.0, 0.0, 0.0 } ));
		Movement_SetGravity(client, 1.0);

		// Don't drop the player off of ladders.
		// The game will automatically change the movetype back to MOVETYPE_WALK if it can't find a ladder.
		// Don't change the movetype if it's currently MOVETYPE_NONE, as that means the player is paused.
		if (Movement_GetMovetype(client) != MOVETYPE_NONE)
		{
			Movement_SetMovetype(client, MOVETYPE_LADDER);
		}

		// Prevent noclip exploit
		SetEntProp(client, Prop_Send, "m_CollisionGroup", GOKZ_COLLISION_GROUP_STANDARD);

		// Force duck on player and make sure that the player can't trigger triggers above them.
		// they can still trigger triggers even when we force ducking.
		if (ducked)
		{
			ForcePlayerDuck(client);

			if (ticks < tickCount - 1)
			{
				// Don't trigger triggers
				SetEntProp(client, Prop_Send, "m_CollisionGroup", GOKZ_COLLISION_GROUP_NOTRIGGER);
			}
			else
			{
				// Let the player trigger triggers on the last tick
				SetEntProp(client, Prop_Send, "m_CollisionGroup", GOKZ_COLLISION_GROUP_STANDARD);
			}
		}

		++ticks;
		data = new DataPack();
		data.WriteCell(GetClientUserId(client));
		data.WriteCell(ticks);
		data.WriteCell(tickCount);
		data.WriteCell(ducked);
		RequestFrame(ContinueHoldStill, data);
	}
}

/**
 * Forces the player to instantly duck.
 *
 * @param client		Client index.
 */
stock void ForcePlayerDuck(int client)
{
	// these are both necessary, because on their own the player will sometimes still be in a state that isn't fully ducked.
	SetEntPropFloat(client, Prop_Send, "m_flDuckAmount", 1.0, 0);
	SetEntProp(client, Prop_Send, "m_bDucking", false);
	SetEntProp(client, Prop_Send, "m_bDucked", true);
}

/**
 * Returns whether the player is stuck e.g. in a wall after noclipping.
 *
 * @param client		Client index.
 * @return				Whether player is stuck.
 */
stock bool IsPlayerStuck(int client)
{
	float vecMin[3], vecMax[3], vecOrigin[3];

	GetClientMins(client, vecMin);
	GetClientMaxs(client, vecMax);
	GetClientAbsOrigin(client, vecOrigin);

	TR_TraceHullFilter(vecOrigin, vecOrigin, vecMin, vecMax, MASK_PLAYERSOLID, TraceEntityFilterPlayers);
	return TR_DidHit(); // head in wall ?
}

/**
 * Retrieves the absolute origin of an entity.
 *
 * @param entity			Index of the entity.
 * @param result			Entity's origin if successful.
 * @return					Returns true if successful.
 */
stock bool GetEntityAbsOrigin(int entity, float result[3])
{
	if (!IsValidEntity(entity))
	{
		return false;
	}

	if (!HasEntProp(entity, Prop_Data, "m_vecAbsOrigin"))
	{
		return false;
	}

	GetEntPropVector(entity, Prop_Data, "m_vecAbsOrigin", result);
	return true;
}

/**
 * Retrieves the name of an entity.
 *
 * @param entity			Index of the entity.
 * @param buffer			Buffer to store the name.
 * @param maxlength			Maximum length of the buffer.
 * @return					Number of non-null bytes written.
 */
stock int GetEntityName(int entity, char[] buffer, int maxlength)
{
	return GetEntPropString(entity, Prop_Data, "m_iName", buffer, maxlength);
}

/**
 * Finds an entity by name or by name and classname.
 * Taken from smlib https://github.com/bcserv/smlib
 * This can take anywhere from ~0.2% to ~11% of frametime (i5-7600k) in the worst case scenario where
 * every entity which has a name (4096 of them) is iterated over. Your mileage may vary.
 *
 * @param name				Name of the entity to find.
 * @param className			Optional classname to match along with name.
 * @param ignorePlayers		Ignore player entities.
 * @return					Entity index if successful, INVALID_ENT_REFERENCE if not.
 */
stock int GOKZFindEntityByName(const char[] name, const char[] className = "", bool ignorePlayers = false)
{
	int result = INVALID_ENT_REFERENCE;
	if (className[0] == '\0')
	{
		// HACK: Double the limit to get non-networked entities too.
		// https://developer.valvesoftware.com/wiki/Entity_limit
		int realMaxEntities = GetMaxEntities() * 2;
		int startEntity = 1;
		if (ignorePlayers)
		{
			startEntity = MaxClients + 1;
		}
		for (int entity = startEntity; entity < realMaxEntities; entity++)
		{
			if (!IsValidEntity(entity))
			{
				continue;
			}

			char entName[65];
			GetEntityName(entity, entName, sizeof(entName));
			if (StrEqual(entName, name))
			{
				result = entity;
				break;
			}
		}
	}
	else
	{
		int entity = INVALID_ENT_REFERENCE;
		while ((entity = FindEntityByClassname(entity, className)) != INVALID_ENT_REFERENCE)
		{
			char entName[65];
			GetEntityName(entity, entName, sizeof(entName));
			if (StrEqual(entName, name))
			{
				result = entity;
				break;
			}
		}
	}
	return result;
}

/**
 * Gets the current map's display name in lower case.
 *
 * @param buffer			Buffer to store the map name.
 * @param maxlength			Maximum length of buffer.
 */
stock void GetCurrentMapDisplayName(char[] buffer, int maxlength)
{
	char map[PLATFORM_MAX_PATH];
	GetCurrentMap(map, sizeof(map));
	GetMapDisplayName(map, map, sizeof(map));
	String_ToLower(map, buffer, maxlength);
}

/**
 * Gets the current map's file size.
 */
stock int GetCurrentMapFileSize()
{
	char mapBuffer[PLATFORM_MAX_PATH];
	GetCurrentMap(mapBuffer, sizeof(mapBuffer));
	Format(mapBuffer, sizeof(mapBuffer), "maps/%s.bsp", mapBuffer);
	return FileSize(mapBuffer);
}

/**
 * Copies the elements of a source vector to a destination vector.
 *
 * @param src				Source vector.
 * @param dest				Destination vector.
 */
stock void CopyVector(const any src[3], any dest[3])
{
	dest[0] = src[0];
	dest[1] = src[1];
	dest[2] = src[2];
}

/**
 * Returns whether the player is spectating.
 *
 * @param client		Client index.
 */
stock bool IsSpectating(int client)
{
	int team = GetClientTeam(client);
	return team == CS_TEAM_SPECTATOR || team == CS_TEAM_NONE;
}

/**
 * Rotate a vector on an axis.
 *
 * @param vec				Vector to rotate.
 * @param axis				Axis to rotate around.
 * @param theta				Angle in radians.
 * @param result			Rotated vector.
 */
stock void RotateVectorAxis(float vec[3], float axis[3], float theta, float result[3])
{
	float cosTheta = Cosine(theta);
	float sinTheta = Sine(theta);

	float axisVecCross[3];
	GetVectorCrossProduct(axis, vec, axisVecCross);

	for (int i = 0; i < 3; i++)
	{
		result[i] = (vec[i] * cosTheta) + (axisVecCross[i] * sinTheta) + (axis[i] * GetVectorDotProduct(axis, vec)) * (1.0 - cosTheta);
	}
}

/**
 * Rotate a vector by pitch and yaw.
 *
 * @param vec				Vector to rotate.
 * @param pitch				Pitch angle (in degrees).
 * @param yaw				Yaw angle (in degrees).
 * @param result			Rotated vector.
 */
stock void RotateVectorPitchYaw(float vec[3], float pitch, float yaw, float result[3])
{
	if (pitch != 0.0)
	{
		RotateVectorAxis(vec, view_as<float>({0.0, 1.0, 0.0}), DegToRad(pitch), result);
	}
	if (yaw != 0.0)
	{
		RotateVectorAxis(result, view_as<float>({0.0, 0.0, 1.0}), DegToRad(yaw), result);
	}
}

/**
 * Attempts to return a valid spawn location.
 *
 * @param origin			Spawn origin if found.
 * @param angles			Spawn angles if found.
 * @return					Whether a valid spawn point is found.
 */
stock bool GetValidSpawn(float origin[3], float angles[3])
{
	// Return true if the spawn found is truly valid (not in the ground or out of bounds)
	bool foundValidSpawn;
	bool searchCT;
	float spawnOrigin[3];
	float spawnAngles[3];
	int spawnEntity = -1;
	while (!foundValidSpawn)
	{
		if (searchCT)
		{
			spawnEntity = FindEntityByClassname(spawnEntity, "info_player_counterterrorist");
		}
		else
		{
			spawnEntity = FindEntityByClassname(spawnEntity, "info_player_terrorist");
		}

		if (spawnEntity != -1)
		{
			GetEntPropVector(spawnEntity, Prop_Data, "m_vecOrigin", spawnOrigin);
			GetEntPropVector(spawnEntity, Prop_Data, "m_angRotation", spawnAngles);
			if (IsSpawnValid(spawnOrigin))
			{
				origin = spawnOrigin;
				angles = spawnAngles;
				foundValidSpawn = true;
			}
		}
		else if (!searchCT)
		{
			searchCT = true;
		}
		else
		{
			break;
		}
	}
	return foundValidSpawn;
}

/**
 * Check whether a position is a valid spawn location.
 * A spawn location is considered valid if it is in bounds and not stuck inside the ground.
 *
 * @param origin			Origin vector.
 * @return					Whether the origin is a valid spawn location.
 */
stock bool IsSpawnValid(float origin[3])
{
	Handle trace = TR_TraceHullFilterEx(origin, origin, PLAYER_MINS, PLAYER_MAXS, MASK_PLAYERSOLID, TraceEntityFilterPlayers);
	if (!TR_StartSolid(trace) && !TR_AllSolid(trace) && TR_GetFraction(trace) == 1.0)
	{
		delete trace;
		return true;
	}
	delete trace;
	return false;
}

/**
 * Get an entity's origin, angles, its bounding box's center and the distance from the center to its bounding box's edges.
 *
 * @param entity			Index of the entity.
 * @param origin			Entity's origin.
 * @param center			Center of the entity's bounding box.
 * @param angles			Entity's angles.
 * @param distFromCenter	The distance between the center of the entity's bounding box and its edges.
 */
stock void GetEntityPositions(int entity, float origin[3], float center[3], float angles[3], float distFromCenter[3])
{
	int ent = entity;
	float maxs[3], mins[3];
	GetEntPropVector(ent, Prop_Send, "m_vecOrigin", origin);
	// Take parent entities into account.
	while (GetEntPropEnt(ent, Prop_Send, "moveparent") != -1)
	{
		ent = GetEntPropEnt(ent, Prop_Send, "moveparent");
		float tempOrigin[3];
		GetEntPropVector(ent, Prop_Send, "m_vecOrigin", tempOrigin);
		for (int i = 0; i < 3; i++)
		{
			origin[i] += tempOrigin[i];
		}
	}

	GetEntPropVector(ent, Prop_Data, "m_angRotation", angles);

	GetEntPropVector(ent, Prop_Send, "m_vecMaxs", maxs);
	GetEntPropVector(ent, Prop_Send, "m_vecMins", mins);
	for (int i = 0; i < 3; i++)
	{
		center[i] = origin[i] + (maxs[i] + mins[i]) / 2;
		distFromCenter[i] = (maxs[i] - mins[i]) / 2;
	}
}

/**
 * Find a valid position around a timer.
 *
 * @param entity			Index of the timer entity.
 * @param originDest		Result origin if a valid position is found.
 * @param anglesDest		Result angles if a valid position is found.
 * @return					Whether a valid position is found.
 */
stock bool FindValidPositionAroundTimerEntity(int entity, float originDest[3], float anglesDest[3], bool isButton)
{
	float origin[3], center[3], angles[3], distFromCenter[3];
	GetEntityPositions(entity, origin, center, angles, distFromCenter);
	float extraOffset[3];
	if (isButton) // Test several positions within button press range.
	{
		extraOffset[0] = 32.0;
		extraOffset[1] = 32.0;
		extraOffset[2] = 32.0;
	}
	else // Test positions at the inner surface of the zone.
	{
		extraOffset[0] = -(PLAYER_MAXS[0] - PLAYER_MINS[0]) - 1.03125;
		extraOffset[1] = -(PLAYER_MAXS[1] - PLAYER_MINS[1]) - 1.03125;
		extraOffset[2] = -(PLAYER_MAXS[2] - PLAYER_MINS[2]) - 1.03125;
	}
	if (FindValidPositionAroundCenter(center, distFromCenter, extraOffset, originDest, anglesDest))
	{
		return true;
	}
	// Test the positions right next to the timer button/zones if the tests above fail.
	// This can fail when the timer has a cover brush over it.
	extraOffset[0] = 0.03125;
	extraOffset[1] = 0.03125;
	extraOffset[2] = 0.03125;
	return FindValidPositionAroundCenter(center, distFromCenter, extraOffset, originDest, anglesDest);
}

static bool FindValidPositionAroundCenter(float center[3], float distFromCenter[3], float extraOffset[3], float originDest[3], float anglesDest[3])
{
	float testOrigin[3];
	int x, y;

	for (int i = 0; i < 3; i++)
	{
		// The search starts from the center then outwards to opposite directions.
		x = i == 2 ? -1 : i;
		for (int j = 0; j < 3; j++)
		{
			y = j == 2 ? -1 : j;
			for (int z = -1; z <= 1; z++)
			{
				testOrigin = center;
				testOrigin[0] = testOrigin[0] + (distFromCenter[0] + extraOffset[0]) * x + (PLAYER_MAXS[0] - PLAYER_MINS[0]) * x * 0.5;
				testOrigin[1] = testOrigin[1] + (distFromCenter[1] + extraOffset[1]) * y + (PLAYER_MAXS[1] - PLAYER_MINS[1]) * y * 0.5;
				testOrigin[2] = testOrigin[2] + (distFromCenter[2] + extraOffset[2]) * z + (PLAYER_MAXS[2] - PLAYER_MINS[2]) * z;

				// Check if there's a line of sight towards the zone as well.
				if (IsSpawnValid(testOrigin) && CanSeeBox(testOrigin, center, distFromCenter))
				{
					originDest = testOrigin;
					// Always look towards the center.
					float offsetVector[3];
					offsetVector[0] = -(distFromCenter[0] + extraOffset[0]) * x;
					offsetVector[1] = -(distFromCenter[1] + extraOffset[1]) * y;
					offsetVector[2] = -(distFromCenter[2] + extraOffset[2]) * z;
					GetVectorAngles(offsetVector, anglesDest);
					anglesDest[2] = 0.0; // Roll should always be 0.0
					return true;
				}
			}
		}
	}
	return false;
}

static bool CanSeeBox(float origin[3], float center[3], float distFromCenter[3])
{
	float traceOrigin[3], traceDest[3], mins[3], maxs[3];

	CopyVector(origin, traceOrigin);


	SubtractVectors(center, distFromCenter, mins);
	AddVectors(center, distFromCenter, maxs);

	for (int i = 0; i < 3; i++)
	{
		mins[i] += 0.03125;
		maxs[i] -= 0.03125;
		traceDest[i] = FloatClamp(traceOrigin[i], mins[i], maxs[i]);
	}
	int mask = (MASK_NPCSOLID_BRUSHONLY | MASK_OPAQUE_AND_NPCS) & ~CONTENTS_OPAQUE;
	Handle trace = TR_TraceRayFilterEx(traceOrigin, traceDest, mask, RayType_EndPoint, TraceEntityFilterPlayers);
	if (TR_DidHit(trace))
	{
		float end[3];
		TR_GetEndPosition(end, trace);
		for (int i = 0; i < 3; i++)
		{
			if (end[i] != traceDest[i])
			{
				delete trace;
				return false;
			}
		}
	}
	delete trace;
	return true;
}

/**
 * Gets entity index from the address to an entity.
 *
 * @param pEntity			Entity address.
 * @return					Entity index.
 * @error					Couldn't find offset for m_angRotation, m_vecViewOffset, couldn't confirm offset of m_RefEHandle.
 */
stock int GOKZGetEntityFromAddress(Address pEntity)
{
	static int offs_RefEHandle;
	if (offs_RefEHandle)
	{
		return EntRefToEntIndex(LoadFromAddress(pEntity + view_as<Address>(offs_RefEHandle), NumberType_Int32) | (1 << 31));
	}

	// if we don't have it already, attempt to lookup offset based on SDK information
	// CWorld is derived from CBaseEntity so it should have both offsets
	int offs_angRotation = FindDataMapInfo(0, "m_angRotation"), offs_vecViewOffset = FindDataMapInfo(0, "m_vecViewOffset");
	if (offs_angRotation == -1)
	{
		SetFailState("Could not find offset for ((CBaseEntity) CWorld)::m_angRotation");
	}
	else if (offs_vecViewOffset == -1)
	{
		SetFailState("Could not find offset for ((CBaseEntity) CWorld)::m_vecViewOffset");
	}
	else if ((offs_angRotation + 0x0C) != (offs_vecViewOffset - 0x04))
	{
		char game[32];
		GetGameFolderName(game, sizeof(game));
		SetFailState("Could not confirm offset of CBaseEntity::m_RefEHandle (incorrect assumption for game '%s'?)", game);
	}

	// offset seems right, cache it for the next call
	offs_RefEHandle = offs_angRotation + 0x0C;
	return GOKZGetEntityFromAddress(pEntity);
}

/**
 * Gets client index from CGameMovement class.
 *
 * @param addr							Address of CGameMovement class.
 * @param offsetCGameMovement_player	Offset of CGameMovement::player.
 * @return								Client index.
 * @error								Couldn't find offset for m_angRotation, m_vecViewOffset, couldn't confirm offset of m_RefEHandle.
 */
stock int GOKZGetClientFromGameMovementAddress(Address addr, int offsetCGameMovement_player)
{
	Address playerAddr = view_as<Address>(LoadFromAddress(view_as<Address>(view_as<int>(addr) + offsetCGameMovement_player), NumberType_Int32));
	return GOKZGetEntityFromAddress(playerAddr);
}

/**
 * Gets the nearest point in the oriented bounding box of an entity to a point.
 *
 * @param entity			Entity index.
 * @param origin			Point's origin.
 * @param result			Result point.
 */
stock void CalcNearestPoint(int entity, float origin[3], float result[3])
{
	float entOrigin[3], entMins[3], entMaxs[3], trueMins[3], trueMaxs[3];
	GetEntPropVector(entity, Prop_Send, "m_vecOrigin", entOrigin);
	GetEntPropVector(entity, Prop_Send, "m_vecMaxs", entMaxs);
	GetEntPropVector(entity, Prop_Send, "m_vecMins", entMins);

	AddVectors(entOrigin, entMins, trueMins);
	AddVectors(entOrigin, entMaxs, trueMaxs);

	for (int i = 0; i < 3; i++)
	{
		result[i] = FloatClamp(origin[i], trueMins[i], trueMaxs[i]);
	}
}

/**
 * Get the shortest distance from P to the (infinite) line through vLineA and vLineB.
 *
 * @param P				Point's origin.
 * @param vLineA		Origin of the first point of the line.
 * @param vLineB		Origin of the first point of the line.
 * @return				The shortest distance from the point to the line.
 */
stock float CalcDistanceToLine(float P[3], float vLineA[3], float vLineB[3])
{
	float vClosest[3];
	float vDir[3];
	float t;
	float delta[3];	
	SubtractVectors(vLineB, vLineA, vDir);
	float div = GetVectorDotProduct(vDir, vDir);
	if (div < EPSILON)
	{
		t = 0.0;
	}
	else
	{
		t = (GetVectorDotProduct(vDir, P) - GetVectorDotProduct(vDir, vLineA)) / div;
	}
	for (int i = 0; i < 3; i++)
	{
		vClosest[i] = vLineA[i] + vDir[i]*t;
	}
	SubtractVectors(P, vClosest, delta);
	return GetVectorLength(delta);
}

/**
 * Gets the ideal amount of time the text should be held for HUD messages.
 * 
 * The message buffer is only 16 slots long, and it is shared between 6 channels maximum.
 * Assuming a message is sent every game frame, each channel used should be only taking around 2.5 slots on average.
 * This also assumes all channels are used equally (so no other plugin taking all the channel buffer for itself).
 * We want to use as much of the message buffer as possible to take into account latency variances.
 * 
 * @param interval						HUD message update interval, in tick intervals.
 * @return								How long the text should be held for.
 */
stock float GetTextHoldTime(int interval)
{
    return 3 * interval * GetTickInterval();
}