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