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