]> git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/common/gamemodes/gamemode/lms/sv_lms.qc
LMS: add max player respawn delay setting
[xonotic/xonotic-data.pk3dir.git] / qcsrc / common / gamemodes / gamemode / lms / sv_lms.qc
1 #include "sv_lms.qh"
2
3 #include <common/mutators/mutator/instagib/items.qh>
4 #include <server/campaign.qh>
5 #include <server/command/_mod.qh>
6 #include <server/world.qh>
7 #include <server/items/items.qh>
8
9 int autocvar_g_lms_extra_lives;
10 bool autocvar_g_lms_join_anytime;
11 int autocvar_g_lms_last_join;
12 bool autocvar_g_lms_regenerate;
13 int autocvar_g_lms_leader_wp_lives;
14 float autocvar_g_lms_leader_wp_max_relative;
15 float autocvar_g_lms_leader_wp_time;
16 float autocvar_g_lms_leader_wp_time_repeat;
17 float autocvar_g_lms_dynamic_respawn_delay;
18 float autocvar_g_lms_dynamic_respawn_delay_base;
19 float autocvar_g_lms_dynamic_respawn_delay_increase;
20 float autocvar_g_lms_dynamic_respawn_delay_max;
21 bool autocvar_g_lms_dynamic_vampire;
22 float autocvar_g_lms_dynamic_vampire_factor_base;
23 float autocvar_g_lms_dynamic_vampire_factor_increase;
24 float autocvar_g_lms_dynamic_vampire_factor_max;
25 int autocvar_g_lms_dynamic_vampire_min_lives_diff;
26
27 .float lms_leader;
28 int lms_leaders;
29 bool lms_visible_leaders;
30 bool lms_visible_leaders_prev;
31
32 // main functions
33 int LMS_NewPlayerLives()
34 {
35         int fl = floor(autocvar_fraglimit);
36         if(fl == 0 || fl > 999)
37                 fl = 999;
38
39         // first player has left the game for dying too much? Nobody else can get in.
40         if(lms_lowest_lives < 1)
41                 return 0;
42
43         if(!autocvar_g_lms_join_anytime)
44                 if(lms_lowest_lives < fl - max(0, floor(autocvar_g_lms_last_join)))
45                         return 0;
46
47         return bound(1, lms_lowest_lives, fl);
48 }
49
50 void ClearWinners();
51
52 // LMS winning condition: game terminates if and only if there's at most one
53 // one player who's living lives. Top two scores being equal cancels the time
54 // limit.
55 int WinningCondition_LMS()
56 {
57         if (warmup_stage || time <= game_starttime)
58                 return WINNING_NO;
59
60         entity first_player = NULL;
61         int totalplayers = 0;
62         FOREACH_CLIENT(IS_PLAYER(it) && it.frags == FRAGS_PLAYER, {
63                 if (!totalplayers)
64                         first_player = it;
65                 ++totalplayers;
66         });
67
68         if (totalplayers)
69         {
70                 if (totalplayers > 1)
71                 {
72                         // two or more active players - continue with the game
73
74                         if (autocvar_g_campaign)
75                         {
76                                 FOREACH_CLIENT(IS_REAL_CLIENT(it), {
77                                         float pl_lives = GameRules_scoring_add(it, LMS_LIVES, 0);
78                                         if (!pl_lives)
79                                                 return WINNING_YES; // human player lost, game over
80                                         break;
81                                 });
82                         }
83                 }
84                 else
85                 {
86                         // exactly one player?
87
88                         ClearWinners();
89                         SetWinners(winning, 0); // NOTE: exactly one player is still "player", so this works out
90
91                         if (LMS_NewPlayerLives())
92                         {
93                                 // game still running (that is, nobody got removed from the game by a frag yet)? then continue
94                                 return WINNING_NO;
95                         }
96                         else
97                         {
98                                 // a winner!
99                                 // and assign him his first place
100                                 GameRules_scoring_add(first_player, LMS_RANK, 1);
101                                 return WINNING_YES;
102                         }
103                 }
104         }
105         else
106         {
107                 // nobody is playing at all...
108                 if (LMS_NewPlayerLives())
109                 {
110                         // wait for players...
111                 }
112                 else
113                 {
114                         // SNAFU (maybe a draw game?)
115                         ClearWinners();
116                         LOG_TRACE("No players, ending game.");
117                         return WINNING_YES;
118                 }
119         }
120
121         // When we get here, we have at least two players who are actually LIVING,
122         // now check if the top two players have equal score.
123         WinningConditionHelper(NULL);
124
125         ClearWinners();
126         if(WinningConditionHelper_winner)
127                 WinningConditionHelper_winner.winning = true;
128         if(WinningConditionHelper_topscore == WinningConditionHelper_secondscore)
129                 return WINNING_NEVER;
130
131         // Top two have different scores? Way to go for our beloved TIMELIMIT!
132         return WINNING_NO;
133 }
134
135 // runs on waypoints which are attached to leaders, updates once per frame
136 bool lms_waypointsprite_visible_for_player(entity this, entity player, entity view)
137 {
138         if(view.lms_leader)
139                 if(IS_SPEC(player))
140                         return false; // we don't want spectators of leaders to see the attached waypoint on the top of their screen
141
142         if (!lms_visible_leaders)
143                 return false;
144
145         return true;
146 }
147
148 void lms_UpdateLeaders()
149 {
150         int max_lives = 0;
151         int pl_cnt = 0;
152         FOREACH_CLIENT(IS_PLAYER(it) && it.frags != FRAGS_PLAYER_OUT_OF_GAME, {
153                 int lives = GameRules_scoring_add(it, LMS_LIVES, 0);
154                 if (lives > max_lives)
155                         max_lives = lives;
156                 pl_cnt++;
157         });
158
159         int second_max_lives = 0;
160         int pl_cnt_with_max_lives = 0;
161         FOREACH_CLIENT(IS_PLAYER(it) && it.frags != FRAGS_PLAYER_OUT_OF_GAME, {
162                 int lives = GameRules_scoring_add(it, LMS_LIVES, 0);
163                 if (lives == max_lives)
164                         pl_cnt_with_max_lives++;
165                 else if (lives > second_max_lives)
166                         second_max_lives = lives;
167         });
168
169         int lives_diff = autocvar_g_lms_leader_wp_lives;
170         if (max_lives - second_max_lives >= lives_diff && pl_cnt_with_max_lives <= pl_cnt * autocvar_g_lms_leader_wp_max_relative)
171                 FOREACH_CLIENT(IS_PLAYER(it) && it.frags != FRAGS_PLAYER_OUT_OF_GAME, {
172                         int lives = GameRules_scoring_add(it, LMS_LIVES, 0);
173                         if (lives == max_lives)
174                         {
175                                 if (!it.lms_leader)
176                                         it.lms_leader = true;
177                         }
178                         else
179                         {
180                                 it.lms_leader = false;
181                         }
182                 });
183         else
184                 FOREACH_CLIENT(IS_PLAYER(it) && it.frags != FRAGS_PLAYER_OUT_OF_GAME, {
185                         if (it.waypointsprite_attachedforcarrier)
186                                 WaypointSprite_Kill(it.waypointsprite_attachedforcarrier);
187                         it.lms_leader = false;
188                 });
189 }
190
191 // mutator hooks
192 MUTATOR_HOOKFUNCTION(lms, reset_map_global)
193 {
194         lms_lowest_lives = 999;
195 }
196
197 MUTATOR_HOOKFUNCTION(lms, reset_map_players)
198 {
199         FOREACH_CLIENT(true, {
200                 if (it.frags == FRAGS_PLAYER_OUT_OF_GAME)
201                 {
202                         // players who forfeited (rank >= 256) become spectators
203                         if (it.lms_spectate_warning == 2)
204                                 it.frags = FRAGS_SPECTATOR;
205                         else
206                                 it.frags = FRAGS_PLAYER;
207                 }
208
209                 CS(it).killcount = 0;
210                 it.lmsplayer = 0;
211                 it.lms_spectate_warning = 0;
212                 GameRules_scoring_add(it, LMS_RANK, -GameRules_scoring_add(it, LMS_RANK, 0));
213                 GameRules_scoring_add(it, LMS_LIVES, -GameRules_scoring_add(it, LMS_LIVES, 0));
214
215                 if (it.frags != FRAGS_PLAYER)
216                         continue;
217
218                 TRANSMUTE(Player, it);
219                 PutClientInServer(it);
220                 it.lms_leader = false;
221                 if (it.waypointsprite_attachedforcarrier)
222                         WaypointSprite_Kill(it.waypointsprite_attachedforcarrier);
223         });
224 }
225
226 // FIXME add support for sv_ready_restart_after_countdown
227 // that is find a way to respawn/reset players IN GAME without setting lives to 0
228 MUTATOR_HOOKFUNCTION(lms, ReadLevelCvars)
229 {
230         // incompatible
231         sv_ready_restart_after_countdown = 0;
232 }
233
234 // returns true if player is added to the game
235 bool lms_AddPlayer(entity player)
236 {
237         if (!player.lmsplayer)
238         {
239                 int lives = GameRules_scoring_add(player, LMS_LIVES, LMS_NewPlayerLives());
240                 if(lives <= 0)
241                         return false;
242                 player.lmsplayer = 2; // temp value indicating player has just joined the game (but not spawned yet)
243         }
244         if (warmup_stage || time <= game_starttime)
245         {
246                 if(player.lms_spectate_warning)
247                 {
248                         player.lms_spectate_warning = 0;
249                         GameRules_scoring_add(player, LMS_RANK, -GameRules_scoring_add(player, LMS_RANK, 0));
250                         int lives = GameRules_scoring_add(player, LMS_LIVES, 0);
251                         if(lives <= 0)
252                                 GameRules_scoring_add(player, LMS_LIVES, LMS_NewPlayerLives());
253                 }
254         }
255         else
256         {
257                 if(GameRules_scoring_add(player, LMS_LIVES, 0) <= 0)
258                 {
259                         Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_LMS_NOLIVES);
260                         return false;
261                 }
262         }
263         return true;
264 }
265
266 MUTATOR_HOOKFUNCTION(lms, PutClientInServer)
267 {
268         entity player = M_ARGV(0, entity);
269         if (!warmup_stage && (IS_BOT_CLIENT(player) || CS(player).jointime != time))
270         {
271                 if (GameRules_scoring_add(player, LMS_RANK, 0) || !lms_AddPlayer(player))
272                         TRANSMUTE(Observer, player);
273         }
274 }
275
276 MUTATOR_HOOKFUNCTION(lms, PlayerSpawn)
277 {
278         entity player = M_ARGV(0, entity);
279
280         if (warmup_stage || time < game_starttime)
281                 return true;
282
283         if (player.lmsplayer == 2) // just joined the game
284         {
285                 // spawn player with the same amount of health / armor
286                 // as the least healthy player with the least number of lives
287                 int pl_lives = GameRules_scoring_add(player, LMS_LIVES, 0);
288                 float min_health = start_health;
289                 float min_armorvalue = start_armorvalue;
290                 FOREACH_CLIENT(it != player && IS_PLAYER(it) && !IS_DEAD(it) && GameRules_scoring_add(it, LMS_LIVES, 0) == pl_lives, {
291                         if (GetResource(it, RES_HEALTH) < min_health)
292                                 min_health = GetResource(it, RES_HEALTH);
293                         if (GetResource(it, RES_ARMOR) < min_armorvalue)
294                                 min_armorvalue = GetResource(it, RES_ARMOR);
295                 });
296                 if (min_health != start_health)
297                         SetResource(player, RES_HEALTH, max(1, min_health));
298                 if (min_armorvalue != start_armorvalue)
299                         SetResource(player, RES_ARMOR, min_armorvalue);
300                 player.lmsplayer = 1;
301         }
302 }
303
304 MUTATOR_HOOKFUNCTION(lms, ForbidSpawn)
305 {
306         entity player = M_ARGV(0, entity);
307
308         if (warmup_stage || lms_AddPlayer(player))
309                 return false;
310
311         return true;
312 }
313
314 void lms_RemovePlayer(entity player)
315 {
316         if (warmup_stage || time < game_starttime)
317                 return;
318
319         float player_rank = GameRules_scoring_add(player, LMS_RANK, 0);
320         if (!player_rank)
321         {
322                 if (player.lms_spectate_warning < 2)
323                 {
324                         player.frags = FRAGS_PLAYER_OUT_OF_GAME;
325                         int pl_cnt = 0;
326                         FOREACH_CLIENT(IS_PLAYER(it) && it.frags == FRAGS_PLAYER, {
327                                 pl_cnt++;
328                         });
329                         GameRules_scoring_add(player, LMS_RANK, pl_cnt + 1);
330                 }
331                 else
332                 {
333                         int min_forfeiter_rank = 665; // different from 666
334                         FOREACH_CLIENT(true, {
335                                 // update rank of other players that were eliminated
336                                 if (it.frags == FRAGS_PLAYER_OUT_OF_GAME)
337                                 {
338                                         float it_rank = GameRules_scoring_add(it, LMS_RANK, 0);
339                                         if (it_rank > player_rank && it_rank <= 256)
340                                                 GameRules_scoring_add(it, LMS_RANK, -1);
341                                         if (it_rank > 256 && it_rank <= min_forfeiter_rank)
342                                                 min_forfeiter_rank = it_rank - 1;
343                                 }
344                                 else if (it.frags != FRAGS_SPECTATOR)
345                                 {
346                                         float tl = GameRules_scoring_add(it, LMS_LIVES, 0);
347                                         if(tl < lms_lowest_lives)
348                                                 lms_lowest_lives = tl;
349                                 }
350                         });
351                         GameRules_scoring_add(player, LMS_RANK, min_forfeiter_rank);
352                         if(!warmup_stage)
353                                 GameRules_scoring_add(player, LMS_LIVES, -GameRules_scoring_add(player, LMS_LIVES, 0));
354                         player.frags = FRAGS_PLAYER_OUT_OF_GAME;
355                         TRANSMUTE(Observer, player);
356                 }
357                 if (autocvar_g_lms_leader_wp_lives > 0)
358                         lms_UpdateLeaders();
359         }
360
361         if (CS(player).killcount != FRAGS_SPECTATOR && player.lms_spectate_warning < 3)
362         {
363                 if (GameRules_scoring_add(player, LMS_RANK, 0) > 0 && player.lms_spectate_warning < 2)
364                         Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_LMS_NOLIVES, player.netname);
365                 else
366                         Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_LMS_FORFEIT, player.netname);
367         }
368 }
369
370 MUTATOR_HOOKFUNCTION(lms, ClientDisconnect)
371 {
372         entity player = M_ARGV(0, entity);
373
374         // no further message other than the disconnect message
375         player.lms_spectate_warning = 3;
376
377         lms_RemovePlayer(player);
378         player.lmsplayer = 0;
379 }
380
381 MUTATOR_HOOKFUNCTION(lms, MakePlayerObserver)
382 {
383         entity player = M_ARGV(0, entity);
384         bool is_forced = M_ARGV(1, bool);
385
386         if (!IS_PLAYER(player))
387                 return true;
388
389         if (warmup_stage || time <= game_starttime)
390         {
391                 GameRules_scoring_add(player, LMS_LIVES, -GameRules_scoring_add(player, LMS_LIVES, 0));
392                 player.frags = FRAGS_SPECTATOR;
393                 TRANSMUTE(Observer, player);
394                 player.lmsplayer = 0;
395         }
396         else
397         {
398                 if (is_forced)
399                         player.lms_spectate_warning = 2;
400                 if (!GameRules_scoring_add(player, LMS_RANK, 0))
401                         lms_RemovePlayer(player);
402         }
403         return true;  // prevent team reset
404 }
405
406 MUTATOR_HOOKFUNCTION(lms, ClientConnect)
407 {
408         entity player = M_ARGV(0, entity);
409         player.frags = FRAGS_SPECTATOR;
410 }
411
412 MUTATOR_HOOKFUNCTION(lms, PlayerPreThink)
413 {
414         entity player = M_ARGV(0, entity);
415
416         // recycled REDALIVE to avoid adding a dedicated stat
417         STAT(REDALIVE, player) = lms_leaders;
418
419         if(player.deadflag == DEAD_DYING)
420                 player.deadflag = DEAD_RESPAWNING;
421 }
422
423 MUTATOR_HOOKFUNCTION(lms, SV_StartFrame)
424 {
425         float leader_time = autocvar_g_lms_leader_wp_time;
426         float leader_repeat_time = leader_time + autocvar_g_lms_leader_wp_time_repeat;
427         lms_visible_leaders_prev = lms_visible_leaders;
428         lms_visible_leaders = (time % leader_repeat_time < leader_time);
429
430         lms_leaders = 0;
431         FOREACH_CLIENT(true, {
432                 STAT(OBJECTIVE_STATUS, it) = lms_visible_leaders;
433                 if (IS_PLAYER(it) && it.frags != FRAGS_PLAYER_OUT_OF_GAME)
434                 {
435                         if (it.lms_leader)
436                         {
437                                 if (!it.waypointsprite_attachedforcarrier)
438                                 {
439                                         WaypointSprite_AttachCarrier(WP_LmsLeader, it, RADARICON_FLAGCARRIER);
440                                         it.waypointsprite_attachedforcarrier.waypointsprite_visible_for_player = lms_waypointsprite_visible_for_player;
441                                         WaypointSprite_UpdateRule(it.waypointsprite_attachedforcarrier, 0, SPRITERULE_DEFAULT);
442                                         vector pl_color = colormapPaletteColor(it.clientcolors & 0x0F, false);
443                                         WaypointSprite_UpdateTeamRadar(it.waypointsprite_attachedforcarrier, RADARICON_FLAGCARRIER, pl_color);
444                                         WaypointSprite_Ping(it.waypointsprite_attachedforcarrier);
445                                 }
446                                 if (!lms_visible_leaders_prev && lms_visible_leaders && IS_REAL_CLIENT(it))
447                                         Send_Notification(NOTIF_ONE, it, MSG_CENTER, CENTER_LMS_VISIBLE_LEADER);
448                                 lms_leaders++;
449                         }
450                 }
451                 else
452                 {
453                         if (IS_PLAYER(it) && it.frags != FRAGS_PLAYER_OUT_OF_GAME)
454                         {
455                                 if (!lms_visible_leaders_prev && lms_visible_leaders && IS_REAL_CLIENT(it))
456                                         Send_Notification(NOTIF_ONE, it, MSG_CENTER, CENTER_LMS_VISIBLE_OTHER);
457                         }
458                         if (it.waypointsprite_attachedforcarrier)
459                                 WaypointSprite_Kill(it.waypointsprite_attachedforcarrier);
460                 }
461         });
462 }
463
464 MUTATOR_HOOKFUNCTION(lms, PlayerRegen)
465 {
466         if(autocvar_g_lms_regenerate)
467                 return false;
468         return true;
469 }
470
471 MUTATOR_HOOKFUNCTION(lms, PlayerPowerups)
472 {
473         entity player = M_ARGV(0, entity);
474         if (player.waypointsprite_attachedforcarrier)
475                 player.effects |= (EF_ADDITIVE | EF_FULLBRIGHT);
476         else
477                 player.effects &= ~(EF_ADDITIVE | EF_FULLBRIGHT);
478 }
479
480 MUTATOR_HOOKFUNCTION(lms, ForbidThrowCurrentWeapon)
481 {
482         // forbode!
483         return true;
484 }
485
486 MUTATOR_HOOKFUNCTION(lms, Damage_Calculate)
487 {
488         if (!autocvar_g_lms_dynamic_vampire)
489                 return;
490
491         entity frag_attacker = M_ARGV(1, entity);
492         entity frag_target = M_ARGV(2, entity);
493         float frag_damage = M_ARGV(4, float);
494
495         if (IS_PLAYER(frag_attacker) && !IS_DEAD(frag_attacker)
496                 && IS_PLAYER(frag_target) && !IS_DEAD(frag_target) && frag_attacker != frag_target)
497         {
498                 float vampire_factor = 0;
499
500                 int frag_attacker_lives = GameRules_scoring_add(frag_attacker, LMS_LIVES, 0);
501                 int frag_target_lives = GameRules_scoring_add(frag_target, LMS_LIVES, 0);
502                 int diff = frag_target_lives - frag_attacker_lives - autocvar_g_lms_dynamic_vampire_min_lives_diff;
503
504                 if (diff >= 0)
505                         vampire_factor = autocvar_g_lms_dynamic_vampire_factor_base + diff * autocvar_g_lms_dynamic_vampire_factor_increase;
506                 if (vampire_factor > 0)
507                 {
508                         vampire_factor = min(vampire_factor, autocvar_g_lms_dynamic_vampire_factor_max);
509                         SetResourceExplicit(frag_attacker, RES_HEALTH,
510                                 min(GetResource(frag_attacker, RES_HEALTH) + frag_damage * vampire_factor, start_health));
511                 }
512         }
513 }
514
515 MUTATOR_HOOKFUNCTION(lms, PlayerDied)
516 {
517         if (!warmup_stage && autocvar_g_lms_leader_wp_lives > 0)
518                 lms_UpdateLeaders();
519 }
520
521 MUTATOR_HOOKFUNCTION(lms, CalculateRespawnTime)
522 {
523         entity player = M_ARGV(0, entity);
524         player.respawn_flags |= RESPAWN_FORCE;
525
526         int pl_lives = GameRules_scoring_add(player, LMS_LIVES, 0);
527         if (pl_lives <= 0)
528         {
529                 player.respawn_flags = RESPAWN_SILENT;
530                 // prevent unwanted sudden rejoin as spectator and movement of spectator camera
531                 player.respawn_time = time + 2;
532                 return true;
533         }
534
535         if (autocvar_g_lms_dynamic_respawn_delay <= 0)
536                 return false;
537
538         int max_lives = 0;
539         int pl_cnt = 0;
540         FOREACH_CLIENT(it != player && IS_PLAYER(it) && it.frags != FRAGS_PLAYER_OUT_OF_GAME, {
541                 int lives = GameRules_scoring_add(it, LMS_LIVES, 0);
542                 if (lives > max_lives)
543                         max_lives = lives;
544                 pl_cnt++;
545         });
546
547         // min delay with only 2 players
548         if (pl_cnt == 1) // player wasn't counted
549                 max_lives = 0;
550
551         float dlay = autocvar_g_lms_dynamic_respawn_delay_base +
552                 autocvar_g_lms_dynamic_respawn_delay_increase * max(0, max_lives - pl_lives);
553         player.respawn_time = time + min(autocvar_g_lms_dynamic_respawn_delay_max, dlay);
554         return true;
555 }
556
557 MUTATOR_HOOKFUNCTION(lms, GiveFragsForKill)
558 {
559         entity frag_target = M_ARGV(1, entity);
560
561         if (!warmup_stage && time > game_starttime)
562         {
563                 // remove a life
564                 int tl = GameRules_scoring_add(frag_target, LMS_LIVES, -1);
565                 if(tl < lms_lowest_lives)
566                         lms_lowest_lives = tl;
567                 if(tl <= 0)
568                 {
569                         int pl_cnt = 0;
570                         FOREACH_CLIENT(IS_PLAYER(it) && it.frags == FRAGS_PLAYER, {
571                                 pl_cnt++;
572                         });
573                         frag_target.frags = FRAGS_PLAYER_OUT_OF_GAME;
574                         GameRules_scoring_add(frag_target, LMS_RANK, pl_cnt);
575                 }
576         }
577         M_ARGV(2, float) = 0; // frag score
578
579         return true;
580 }
581
582 MUTATOR_HOOKFUNCTION(lms, SetStartItems)
583 {
584         start_items &= ~(IT_UNLIMITED_AMMO | IT_UNLIMITED_SUPERWEAPONS);
585         start_health       = warmup_start_health       = cvar("g_lms_start_health");
586         start_armorvalue   = warmup_start_armorvalue   = cvar("g_lms_start_armor");
587         start_ammo_shells  = warmup_start_ammo_shells  = cvar("g_lms_start_ammo_shells");
588         start_ammo_nails   = warmup_start_ammo_nails   = cvar("g_lms_start_ammo_nails");
589         start_ammo_rockets = warmup_start_ammo_rockets = cvar("g_lms_start_ammo_rockets");
590         start_ammo_cells   = warmup_start_ammo_cells   = cvar("g_lms_start_ammo_cells");
591         start_ammo_plasma  = warmup_start_ammo_plasma  = cvar("g_lms_start_ammo_plasma");
592         start_ammo_fuel    = warmup_start_ammo_fuel    = cvar("g_lms_start_ammo_fuel");
593 }
594
595 MUTATOR_HOOKFUNCTION(lms, ForbidPlayerScore_Clear)
596 {
597         // don't clear player score
598         return true;
599 }
600
601 MUTATOR_HOOKFUNCTION(lms, FilterItemDefinition)
602 {
603         entity definition = M_ARGV(0, entity);
604
605         if (autocvar_g_lms_extra_lives && definition == ITEM_ExtraLife)
606         {
607                 return false;
608         }
609         return true;
610 }
611
612 void lms_extralife(entity this)
613 {
614         StartItem(this, ITEM_ExtraLife);
615 }
616
617 MUTATOR_HOOKFUNCTION(lms, OnEntityPreSpawn)
618 {
619         if (MUTATOR_RETURNVALUE) return false;
620         if (!autocvar_g_powerups) return false;
621         if (!autocvar_g_lms_extra_lives) return false;
622
623         entity ent = M_ARGV(0, entity);
624
625         // Can't use .itemdef here
626         if (ent.classname != "item_health_mega") return false;
627
628         entity e = spawn();
629         setthink(e, lms_extralife);
630
631         e.nextthink = time + 0.1;
632         e.spawnflags = ent.spawnflags;
633         e.noalign = ent.noalign;
634         setorigin(e, ent.origin);
635
636         return true;
637 }
638
639 MUTATOR_HOOKFUNCTION(lms, ItemTouch)
640 {
641         if(MUTATOR_RETURNVALUE) return false;
642
643         entity item = M_ARGV(0, entity);
644         entity toucher = M_ARGV(1, entity);
645
646         if(item.itemdef == ITEM_ExtraLife)
647         {
648                 Send_Notification(NOTIF_ONE, toucher, MSG_CENTER, CENTER_EXTRALIVES, autocvar_g_lms_extra_lives);
649                 GameRules_scoring_add(toucher, LMS_LIVES, autocvar_g_lms_extra_lives);
650                 return MUT_ITEMTOUCH_PICKUP;
651         }
652
653         return MUT_ITEMTOUCH_CONTINUE;
654 }
655
656 MUTATOR_HOOKFUNCTION(lms, Bot_FixCount, CBC_ORDER_EXCLUSIVE)
657 {
658         FOREACH_CLIENT(IS_REAL_CLIENT(it), {
659                 if (it.lmsplayer && it.lms_spectate_warning < 2)
660                         ++M_ARGV(0, int); // activerealplayers
661                 ++M_ARGV(1, int); // realplayers
662         });
663
664         return true;
665 }
666
667 MUTATOR_HOOKFUNCTION(lms, ClientCommand_Spectate)
668 {
669         entity player = M_ARGV(0, entity);
670
671         if(warmup_stage || time < game_starttime || player.lms_spectate_warning)
672         {
673                 // for the forfeit message...
674                 player.lms_spectate_warning = 2;
675         }
676         else
677         {
678                 if(player.frags != FRAGS_SPECTATOR && player.frags != FRAGS_PLAYER_OUT_OF_GAME)
679                 {
680                         player.lms_spectate_warning = 1;
681                         sprint(player, "WARNING: you won't be able to enter the game again after spectating in LMS. Use the same command again to spectate anyway.\n");
682                 }
683                 return MUT_SPECCMD_RETURN;
684         }
685         return MUT_SPECCMD_CONTINUE;
686 }
687
688 MUTATOR_HOOKFUNCTION(lms, CheckRules_World)
689 {
690         M_ARGV(0, float) = WinningCondition_LMS();
691         return true;
692 }
693
694 MUTATOR_HOOKFUNCTION(lms, SetWeaponArena)
695 {
696         if(M_ARGV(0, string) == "0" || M_ARGV(0, string) == "")
697                 M_ARGV(0, string) = autocvar_g_lms_weaponarena;
698 }
699
700 MUTATOR_HOOKFUNCTION(lms, GetPlayerStatus)
701 {
702         entity player = M_ARGV(0, entity);
703
704         return boolean(player.lmsplayer);
705 }
706
707 MUTATOR_HOOKFUNCTION(lms, AddPlayerScore)
708 {
709         if(game_stopped)
710         if(M_ARGV(0, entity) == SP_LMS_RANK) // score field
711                 return true; // allow writing to this field in intermission as it is needed for newly joining players
712 }
713
714 void lms_Initialize()
715 {
716         lms_lowest_lives = 999;
717 }