]> git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/server/mutators/gamemode_freezetag.qc
Merge branch 'master' into terencehill/ca_arena_mutators
[xonotic/xonotic-data.pk3dir.git] / qcsrc / server / mutators / gamemode_freezetag.qc
1 .float freezetag_frozen_time;
2 .float freezetag_frozen_timeout;
3 .float freezetag_revive_progress;
4 .entity freezetag_ice;
5 #define ICE_MAX_ALPHA 1
6 #define ICE_MIN_ALPHA 0.1
7 float freezetag_teams;
8
9 void freezetag_count_alive_players()
10 {
11         entity e;
12         total_players = redalive = bluealive = yellowalive = pinkalive = 0;
13         FOR_EACH_PLAYER(e) {
14                 if(e.team == COLOR_TEAM1 && e.health >= 1)
15                 {
16                         ++total_players;
17                         if (!e.freezetag_frozen) ++redalive;
18                 }
19                 else if(e.team == COLOR_TEAM2 && e.health >= 1)
20                 {
21                         ++total_players;
22                         if (!e.freezetag_frozen) ++bluealive;
23                 }
24                 else if(e.team == COLOR_TEAM3 && e.health >= 1)
25                 {
26                         ++total_players;
27                         if (!e.freezetag_frozen) ++yellowalive;
28                 }
29                 else if(e.team == COLOR_TEAM4 && e.health >= 1)
30                 {
31                         ++total_players;
32                         if (!e.freezetag_frozen) ++pinkalive;
33                 }
34         }
35         FOR_EACH_REALCLIENT(e) {
36                 e.redalive_stat = redalive;
37                 e.bluealive_stat = bluealive;
38                 e.yellowalive_stat = yellowalive;
39                 e.pinkalive_stat = pinkalive;
40         }
41 }
42 #define FREEZETAG_ALIVE_TEAMS() ((redalive > 0) + (bluealive > 0) + (yellowalive > 0) + (pinkalive > 0))
43 #define FREEZETAG_ALIVE_TEAMS_OK() (FREEZETAG_ALIVE_TEAMS() == freezetag_teams)
44
45 float prev_total_players;
46 float freezetag_CheckTeams()
47 {
48         entity e;
49         if(FREEZETAG_ALIVE_TEAMS_OK())
50         {
51                 if(prev_total_players != -1)
52                 {
53                         FOR_EACH_REALCLIENT(e)
54                                 Send_CSQC_Centerprint_Generic_Expire(e, CPID_WAITING_PLAYERS);
55                 }
56                 prev_total_players = -1;
57                 return 1;
58         }
59         if(prev_total_players != total_players)
60         {
61                 string teams_missing = "";
62                 if(!redalive)   teams_missing = strcat(teams_missing, ColoredTeamName(COLOR_TEAM1), ", ");
63                 if(!bluealive)  teams_missing = strcat(teams_missing, ColoredTeamName(COLOR_TEAM2), ", ");
64                 if(freezetag_teams >= 3)
65                 if(!yellowalive)        teams_missing = strcat(teams_missing, ColoredTeamName(COLOR_TEAM3), ", ");
66                 if(freezetag_teams == 4)
67                 if(!pinkalive)  teams_missing = strcat(teams_missing, ColoredTeamName(COLOR_TEAM4), ", ");
68                 teams_missing = substring(teams_missing, 0, strlen(teams_missing)-2);
69
70                 FOR_EACH_REALCLIENT(e)
71                         Send_CSQC_Centerprint_Generic(e, CPID_WAITING_PLAYERS, strcat("Waiting for players to join...\n\nNeed active players for: ", teams_missing), -1, 0);
72                 prev_total_players = total_players;
73         }
74         return 0;
75 }
76
77 float freezetag_getWinnerTeam()
78 {
79         float winner_team = 0;
80         if(redalive >= 1)
81                 winner_team = COLOR_TEAM1;
82         if(bluealive >= 1)
83         {
84                 if(winner_team) return 0;
85                 winner_team = COLOR_TEAM2;
86         }
87         if(yellowalive >= 1)
88         {
89                 if(winner_team) return 0;
90                 winner_team = COLOR_TEAM3;
91         }
92         if(pinkalive >= 1)
93         {
94                 if(winner_team) return 0;
95                 winner_team = COLOR_TEAM4;
96         }
97         if(winner_team)
98                 return winner_team;
99         return -1; // no player left
100 }
101
102 float freezetag_CheckWinner()
103 {
104         entity e;
105         if(round_handler_GetTimeLeft() <= 0)
106         {
107                 FOR_EACH_REALCLIENT(e)
108                         centerprint(e, "Round over, there's no winner");
109                 bprint("Round over, there's no winner.\n");
110                 FOR_EACH_PLAYER(e)
111                         e.freezetag_frozen_timeout = 0;
112                 round_handler_Init(5, autocvar_g_freezetag_warmup, autocvar_g_freezetag_round_timelimit);
113                 return 1;
114         }
115
116         if(FREEZETAG_ALIVE_TEAMS() > 1)
117                 return 0;
118
119         float winner_team;
120         string teamname;
121         winner_team = freezetag_getWinnerTeam();
122         if(winner_team > 0)
123         {
124                 Send_Notification(NOTIF_ALL, world, MSG_CENTER, APP_TEAM_NUM_4(winner.team, CENTER_FREEZETAG_ROUND_WIN_));
125                 Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_NUM_4(winner.team, INFO_FREEZETAG_ROUND_WIN_));
126                 TeamScore_AddToTeam(winner_team, ST_SCORE, +1);
127         }
128         else if(winner_team == -1)
129         {
130                 FOR_EACH_REALCLIENT(e)
131                         centerprint(e, "^5Round tied! All teams were frozen.");
132                 bprint("^5Round tied! All teams were frozen.\n");
133         }
134
135         FOR_EACH_PLAYER(e)
136                 e.freezetag_frozen_timeout = 0;
137         round_handler_Init(5, autocvar_g_freezetag_warmup, autocvar_g_freezetag_round_timelimit);
138         return 1;
139 }
140
141 // this is needed to allow the player to turn his view around (fixangle can't
142 // be used to freeze his view, as that also changes the angles), while not
143 // turning that ice object with the player
144 void freezetag_Ice_Think()
145 {
146         setorigin(self, self.owner.origin - '0 0 16');
147         self.nextthink = time;
148 }
149
150 void freezetag_Add_Score(entity attacker)
151 {
152         if(attacker == self)
153         {
154                 // you froze your own dumb self
155                 // counted as "suicide" already
156                 PlayerScore_Add(self, SP_SCORE, -1);
157         }
158         else if(attacker.classname == "player")
159         {
160                 // got frozen by an enemy
161                 // counted as "kill" and "death" already
162                 PlayerScore_Add(self, SP_SCORE, -1);
163                 PlayerScore_Add(attacker, SP_SCORE, +1);
164         }
165         // else nothing - got frozen by the game type rules themselves
166 }
167
168 void freezetag_Freeze(entity attacker)
169 {
170         if(self.freezetag_frozen)
171                 return;
172         self.freezetag_frozen = 1;
173         self.freezetag_frozen_time = time;
174         self.freezetag_revive_progress = 0;
175         self.health = 1;
176         if(autocvar_g_freezetag_frozen_maxtime > 0)
177                 self.freezetag_frozen_timeout = time + autocvar_g_freezetag_frozen_maxtime;
178
179         freezetag_count_alive_players();
180
181         entity ice;
182         ice = spawn();
183         ice.owner = self;
184         ice.classname = "freezetag_ice";
185         ice.think = freezetag_Ice_Think;
186         ice.nextthink = time;
187         ice.frame = floor(random() * 21); // ice model has 20 different looking frames
188         ice.alpha = ICE_MAX_ALPHA;
189         ice.colormod = TeamColor(self.team);
190         ice.glowmod = ice.colormod;
191         setmodel(ice, "models/ice/ice.md3");
192
193         self.freezetag_ice = ice;
194
195         RemoveGrapplingHook(self);
196
197         // add waypoint
198         WaypointSprite_Spawn("freezetag_frozen", 0, 0, self, '0 0 64', world, self.team, self, waypointsprite_attached, TRUE, RADARICON_WAYPOINT, '0.25 0.90 1');
199
200         freezetag_Add_Score(attacker);
201 }
202
203 void freezetag_Unfreeze(entity attacker)
204 {
205         self.freezetag_frozen = 0;
206         self.freezetag_frozen_time = 0;
207         self.freezetag_frozen_timeout = 0;
208         self.freezetag_revive_progress = 0;
209
210         remove(self.freezetag_ice);
211         self.freezetag_ice = world;
212
213         if(self.waypointsprite_attached)
214                 WaypointSprite_Kill(self.waypointsprite_attached);
215 }
216
217
218 // ================
219 // Bot player logic
220 // ================
221
222 void() havocbot_role_ft_freeing;
223 void() havocbot_role_ft_offense;
224
225 void havocbot_goalrating_freeplayers(float ratingscale, vector org, float sradius)
226 {
227         entity head;
228         float distance;
229
230         FOR_EACH_PLAYER(head)
231         {
232                 if ((head != self) && (head.team == self.team))
233                 {
234                         if (head.freezetag_frozen)
235                         {
236                                 distance = vlen(head.origin - org);
237                                 if (distance > sradius)
238                                         continue;
239                                 navigation_routerating(head, ratingscale, 2000);
240                         }
241                         else
242                         {
243                                 // If teamate is not frozen still seek them out as fight better
244                                 // in a group.
245                                 navigation_routerating(head, ratingscale/3, 2000);
246                         }
247                 }
248         }
249 }
250
251 void havocbot_role_ft_offense()
252 {
253         entity head;
254         float unfrozen;
255
256         if(self.deadflag != DEAD_NO)
257                 return;
258
259         if (!self.havocbot_role_timeout)
260                 self.havocbot_role_timeout = time + random() * 10 + 20;
261
262         // Count how many players on team are unfrozen.
263         unfrozen = 0;
264         FOR_EACH_PLAYER(head)
265         {
266                 if ((head.team == self.team) && (!head.freezetag_frozen))
267                         unfrozen++;
268         }
269
270         // If only one left on team or if role has timed out then start trying to free players.
271         if (((unfrozen == 0) && (!self.freezetag_frozen)) || (time > self.havocbot_role_timeout))
272         {
273                 dprint("changing role to freeing\n");
274                 self.havocbot_role = havocbot_role_ft_freeing;
275                 self.havocbot_role_timeout = 0;
276                 return;
277         }
278
279         if (time > self.bot_strategytime)
280         {
281                 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
282
283                 navigation_goalrating_start();
284                 havocbot_goalrating_items(10000, self.origin, 10000);
285                 havocbot_goalrating_enemyplayers(20000, self.origin, 10000);
286                 havocbot_goalrating_freeplayers(9000, self.origin, 10000);
287                 //havocbot_goalrating_waypoints(1, self.origin, 1000);
288                 navigation_goalrating_end();
289         }
290 }
291
292 void havocbot_role_ft_freeing()
293 {
294         if(self.deadflag != DEAD_NO)
295                 return;
296
297         if (!self.havocbot_role_timeout)
298                 self.havocbot_role_timeout = time + random() * 10 + 20;
299
300         if (time > self.havocbot_role_timeout)
301         {
302                 dprint("changing role to offense\n");
303                 self.havocbot_role = havocbot_role_ft_offense;
304                 self.havocbot_role_timeout = 0;
305                 return;
306         }
307
308         if (time > self.bot_strategytime)
309         {
310                 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
311
312                 navigation_goalrating_start();
313                 havocbot_goalrating_items(8000, self.origin, 10000);
314                 havocbot_goalrating_enemyplayers(10000, self.origin, 10000);
315                 havocbot_goalrating_freeplayers(20000, self.origin, 10000);
316                 //havocbot_goalrating_waypoints(1, self.origin, 1000);
317                 navigation_goalrating_end();
318         }
319 }
320
321
322 // ==============
323 // Hook Functions
324 // ==============
325
326 MUTATOR_HOOKFUNCTION(freezetag_RemovePlayer)
327 {
328         self.health = 0; // neccessary to update correctly alive stats
329         freezetag_Unfreeze(world);
330         freezetag_count_alive_players();
331         return 1;
332 }
333
334 MUTATOR_HOOKFUNCTION(freezetag_PlayerDies)
335 {
336         if(round_handler_IsActive())
337         if(round_handler_CountdownRunning())
338         {
339                 if(self.freezetag_frozen)
340                         freezetag_Unfreeze(world);
341                 freezetag_count_alive_players();
342                 return 1; // let the player die so that he can respawn whenever he wants
343         }
344
345         // Cases DEATH_TEAMCHANGE and DEATH_AUTOTEAMCHANGE are needed to fix a bug whe
346         // you succeed changing team through the menu: you both really die (gibbing) and get frozen
347         if(ITEM_DAMAGE_NEEDKILL(frag_deathtype)
348                 || frag_deathtype == DEATH_TEAMCHANGE || frag_deathtype == DEATH_AUTOTEAMCHANGE)
349         {
350                 // let the player die, he will be automatically frozen when he respawns
351                 if(!self.freezetag_frozen)
352                 {
353                         freezetag_Add_Score(frag_attacker);
354                         freezetag_count_alive_players();
355                 }
356                 else
357                         freezetag_Unfreeze(world); // remove ice
358                 self.freezetag_frozen_timeout = -2; // freeze on respawn
359                 return 1;
360         }
361
362         if(self.freezetag_frozen)
363                 return 1;
364
365         freezetag_Freeze(frag_attacker);
366
367         if(frag_attacker == frag_target || frag_attacker == world)
368         {
369                 if(frag_target.classname == STR_PLAYER)
370                         Send_Notification(NOTIF_ONE, frag_target, MSG_CENTER, CENTER_FREEZETAG_SELF);
371                 Send_Notification(NOTIF_ALL, world, MSG_INFO, INFO_FREEZETAG_SELF, frag_target.netname);
372         }
373         else
374         {
375                 if(frag_target.classname == STR_PLAYER)
376                         Send_Notification(NOTIF_ONE, frag_target, MSG_CENTER, CENTER_FREEZETAG_FROZEN, frag_attacker.netname);
377                 if(frag_attacker.classname == STR_PLAYER)
378                         Send_Notification(NOTIF_ONE, frag_attacker, MSG_CENTER, CENTER_FREEZETAG_FREEZE, frag_target.netname);
379                 Send_Notification(NOTIF_ALL, world, MSG_INFO, INFO_FREEZETAG_FREEZE, frag_target.netname, frag_attacker.netname);
380         }
381
382         frag_target.health = 1; // "respawn" the player :P
383
384         return 1;
385 }
386
387 MUTATOR_HOOKFUNCTION(freezetag_PlayerSpawn)
388 {
389         if(self.freezetag_frozen_timeout == -1) // if PlayerSpawn is called by reset_map_players
390                 return 1; // do nothing, round is starting right now
391
392         if(self.freezetag_frozen_timeout == -2) // player was dead
393         {
394                 freezetag_Freeze(world);
395                 return 1;
396         }
397
398         freezetag_count_alive_players();
399
400         if(round_handler_IsActive())
401         if(round_handler_IsRoundStarted())
402         {
403                 Send_Notification(NOTIF_ONE, self, MSG_CENTER, CENTER_FREEZETAG_SPAWN_LATE);
404                 freezetag_Freeze(world);
405         }
406
407         return 1;
408 }
409
410 MUTATOR_HOOKFUNCTION(freezetag_reset_map_players)
411 {
412         FOR_EACH_PLAYER(self)
413         {
414                 if (self.freezetag_frozen)
415                         freezetag_Unfreeze(world);
416                 self.freezetag_frozen_timeout = -1;
417                 PutClientInServer();
418                 self.freezetag_frozen_timeout = 0;
419         }
420         freezetag_count_alive_players();
421         return 1;
422 }
423
424 MUTATOR_HOOKFUNCTION(freezetag_GiveFragsForKill)
425 {
426         frag_score = 0; // no frags counted in Freeze Tag
427         return 1;
428 }
429
430 .float reviving; // temp var
431 MUTATOR_HOOKFUNCTION(freezetag_PlayerPreThink)
432 {
433         float n;
434
435         if(gameover)
436                 return 1;
437
438         if(self.freezetag_frozen)
439         {
440                 // keep health = 1
441                 self.pauseregen_finished = time + autocvar_g_balance_pause_health_regen;
442         }
443
444         if(round_handler_IsActive())
445         if(!round_handler_IsRoundStarted())
446                 return 1;
447
448         entity o;
449         o = world;
450         if(self.freezetag_frozen_timeout > 0 && time < self.freezetag_frozen_timeout)
451                 self.freezetag_ice.alpha = ICE_MIN_ALPHA + (ICE_MAX_ALPHA - ICE_MIN_ALPHA) * (self.freezetag_frozen_timeout - time) / (self.freezetag_frozen_timeout - self.freezetag_frozen_time);
452
453         if(self.freezetag_frozen_timeout > 0 && time >= self.freezetag_frozen_timeout)
454                 n = -1;
455         else
456         {
457                 vector revive_extra_size = '1 1 1' * autocvar_g_freezetag_revive_extra_size;
458                 n = 0;
459                 FOR_EACH_PLAYER(other) if(self != other)
460                 {
461                         if(other.freezetag_frozen == 0)
462                         {
463                                 if(other.team == self.team)
464                                 {
465                                         if(boxesoverlap(self.absmin - revive_extra_size, self.absmax + revive_extra_size, other.absmin, other.absmax))
466                                         {
467                                                 if(!o)
468                                                         o = other;
469                                                 if(self.freezetag_frozen)
470                                                         other.reviving = TRUE;
471                                                 ++n;
472                                         }
473                                 }
474                         }
475                 }
476         }
477
478         if(n && self.freezetag_frozen) // OK, there is at least one teammate reviving us
479         {
480                 self.freezetag_revive_progress = bound(0, self.freezetag_revive_progress + frametime * autocvar_g_freezetag_revive_speed, 1);
481                 self.health = max(1, self.freezetag_revive_progress * autocvar_g_balance_health_start);
482
483                 if(self.freezetag_revive_progress >= 1)
484                 {
485                         freezetag_Unfreeze(self);
486                         freezetag_count_alive_players();
487
488                         if(n == -1)
489                         {
490                                 string s = ftos(autocvar_g_freezetag_frozen_maxtime);
491                                 centerprint(self, strcat("^5You were automatically revived after ", s, " seconds^5.\n"));
492                                 bprint("^7", self.netname, "^5 were automatically revived after ", s, " seconds^5.\n");
493                                 return 1;
494                         }
495
496                         // EVERY team mate nearby gets a point (even if multiple!)
497                         FOR_EACH_PLAYER(other)
498                         {
499                                 if(other.reviving)
500                                 {
501                                         PlayerScore_Add(other, SP_FREEZETAG_REVIVALS, +1);
502                                         PlayerScore_Add(other, SP_SCORE, +1);
503                                 }
504                         }
505
506                         Send_Notification(NOTIF_ONE, self, MSG_CENTER, CENTER_FREEZETAG_REVIVED, o.netname);
507                         Send_Notification(NOTIF_ONE, o, MSG_CENTER, CENTER_FREEZETAG_REVIVE, self.netname);
508                         Send_Notification(NOTIF_ALL, world, MSG_INFO, INFO_FREEZETAG_REVIVE, self.netname, o.netname);
509                 }
510
511                 FOR_EACH_PLAYER(other)
512                 {
513                         if(other.reviving)
514                         {
515                                 other.freezetag_revive_progress = self.freezetag_revive_progress;
516                                 other.reviving = FALSE;
517                         }
518                 }
519         }
520         else if(!n && self.freezetag_frozen) // only if no teammate is nearby will we reset
521         {
522                 self.freezetag_revive_progress = bound(0, self.freezetag_revive_progress - frametime * autocvar_g_freezetag_revive_clearspeed, 1);
523                 self.health = max(1, self.freezetag_revive_progress * autocvar_g_balance_health_start);
524         }
525         else if(!n)
526         {
527                 self.freezetag_revive_progress = 0; // thawing nobody
528         }
529
530         return 1;
531 }
532
533 MUTATOR_HOOKFUNCTION(freezetag_PlayerPhysics)
534 {
535         if(self.freezetag_frozen)
536         {
537                 self.movement = '0 0 0';
538                 self.disableclientprediction = 1;
539         }
540         return 1;
541 }
542
543 MUTATOR_HOOKFUNCTION(freezetag_PlayerDamage_Calculate)
544 {
545         if(frag_target.freezetag_frozen && frag_deathtype != DEATH_HURTTRIGGER)
546         {
547                 frag_damage = 0;
548                 frag_force = frag_force * autocvar_g_freezetag_frozen_force;
549         }
550         return 1;
551 }
552
553 MUTATOR_HOOKFUNCTION(freezetag_ForbidThrowCurrentWeapon)
554 {
555         if (self.freezetag_frozen)
556                 return 1;
557         return 0;
558 }
559
560 MUTATOR_HOOKFUNCTION(freezetag_BotRoles)
561 {
562         if not(self.deadflag)
563         {
564                 if (random() < 0.5)
565                         self.havocbot_role = havocbot_role_ft_freeing;
566                 else
567                         self.havocbot_role = havocbot_role_ft_offense;
568         }
569
570         return TRUE;
571 }
572
573 MUTATOR_HOOKFUNCTION(freezetag_SpectateCopy)
574 {
575         self.freezetag_frozen = other.freezetag_frozen;
576         self.freezetag_revive_progress = other.freezetag_revive_progress;
577         return 0;
578 }
579
580 MUTATOR_HOOKFUNCTION(freezetag_GetTeamCount)
581 {
582         freezetag_teams = autocvar_g_freezetag_teams_override;
583         if(freezetag_teams < 2)
584                 freezetag_teams = autocvar_g_freezetag_teams;
585         freezetag_teams = bound(2, freezetag_teams, 4);
586         ret_float = freezetag_teams;
587         return 0;
588 }
589
590 void freezetag_Initialize()
591 {
592         precache_model("models/ice/ice.md3");
593         ScoreRules_freezetag();
594
595         round_handler_Spawn(freezetag_CheckTeams, freezetag_CheckWinner, func_null);
596         round_handler_Init(5, autocvar_g_freezetag_warmup, autocvar_g_freezetag_round_timelimit);
597
598         addstat(STAT_REDALIVE, AS_INT, redalive_stat);
599         addstat(STAT_BLUEALIVE, AS_INT, bluealive_stat);
600         addstat(STAT_YELLOWALIVE, AS_INT, yellowalive_stat);
601         addstat(STAT_PINKALIVE, AS_INT, pinkalive_stat);
602
603         addstat(STAT_FROZEN, AS_INT, freezetag_frozen);
604         addstat(STAT_REVIVE_PROGRESS, AS_FLOAT, freezetag_revive_progress);
605 }
606
607 MUTATOR_DEFINITION(gamemode_freezetag)
608 {
609         MUTATOR_HOOK(MakePlayerObserver, freezetag_RemovePlayer, CBC_ORDER_ANY);
610         MUTATOR_HOOK(ClientDisconnect, freezetag_RemovePlayer, CBC_ORDER_ANY);
611         MUTATOR_HOOK(PlayerDies, freezetag_PlayerDies, CBC_ORDER_ANY);
612         MUTATOR_HOOK(PlayerSpawn, freezetag_PlayerSpawn, CBC_ORDER_ANY);
613         MUTATOR_HOOK(reset_map_players, freezetag_reset_map_players, CBC_ORDER_ANY);
614         MUTATOR_HOOK(GiveFragsForKill, freezetag_GiveFragsForKill, CBC_ORDER_FIRST);
615         MUTATOR_HOOK(PlayerPreThink, freezetag_PlayerPreThink, CBC_ORDER_FIRST);
616         MUTATOR_HOOK(PlayerPhysics, freezetag_PlayerPhysics, CBC_ORDER_FIRST);
617         MUTATOR_HOOK(PlayerDamage_Calculate, freezetag_PlayerDamage_Calculate, CBC_ORDER_ANY);
618         MUTATOR_HOOK(ForbidThrowCurrentWeapon, freezetag_ForbidThrowCurrentWeapon, CBC_ORDER_ANY);
619         MUTATOR_HOOK(HavocBot_ChooseRule, freezetag_BotRoles, CBC_ORDER_ANY);
620         MUTATOR_HOOK(SpectateCopy, freezetag_SpectateCopy, CBC_ORDER_ANY);
621         MUTATOR_HOOK(GetTeamCount, freezetag_GetTeamCount, CBC_ORDER_EXCLUSIVE);
622
623         MUTATOR_ONADD
624         {
625                 if(time > 1) // game loads at time 1
626                         error("This is a game type and it cannot be added at runtime.");
627                 freezetag_Initialize();
628         }
629
630         MUTATOR_ONROLLBACK_OR_REMOVE
631         {
632                 // we actually cannot roll back freezetag_Initialize here
633                 // BUT: we don't need to! If this gets called, adding always
634                 // succeeds.
635         }
636
637         MUTATOR_ONREMOVE
638         {
639                 print("This is a game type and it cannot be removed at runtime.");
640                 return -1;
641         }
642
643         return 0;
644 }