1 /** @file m_cheat.c Cheat code sequences.
2 *
3 * @authors Copyright © 2003-2017 Jaakko Keränen <jaakko.keranen@iki.fi>
4 * @authors Copyright © 2005-2014 Daniel Swanson <danij@dengine.net>
5 * @authors Copyright © 1993-1996 by id Software, Inc.
6 *
7 * @par License
8 * GPL: http://www.gnu.org/licenses/gpl.html
9 *
10 * <small>This program is free software; you can redistribute it and/or modify
11 * it under the terms of the GNU General Public License as published by the
12 * Free Software Foundation; either version 2 of the License, or (at your
13 * option) any later version. This program is distributed in the hope that it
14 * will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
15 * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
16 * Public License for more details. You should have received a copy of the GNU
17 * General Public License along with this program; if not, write to the Free
18 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
19 * 02110-1301 USA</small>
20 */
21
22 #include "m_cheat.h"
23
24 #include <de/Log>
25 #include <de/Range>
26 #include <de/String>
27 #include <de/Vector>
28
29 #include "jdoom.h"
30 #include "d_net.h"
31 #include "d_netcl.h"
32 #include "d_netsv.h"
33 #include "dmu_lib.h"
34 #include "g_eventsequence.h"
35 #include "g_defs.h"
36 #include "gamesession.h"
37 #include "hu_msg.h"
38 #include "p_user.h"
39 #include "p_sound.h"
40 #include "player.h"
41
42 using namespace de;
43
44 typedef eventsequencehandler_t cheatfunc_t;
45
46 /// Helper macro for forming cheat callback function names.
47 #define CHEAT(x) G_Cheat##x
48
49 /// Helper macro for declaring cheat callback functions.
50 #define CHEAT_FUNC(x) int G_Cheat##x(int player, EventSequenceArg const *args, int numArgs)
51
CHEAT_FUNC(Music)52 CHEAT_FUNC(Music)
53 {
54 DENG2_UNUSED(numArgs);
55
56 if(player < 0 || player >= MAXPLAYERS)
57 return false;
58
59 player_t *plr = &players[player];
60
61 int const numEpisodes = PlayableEpisodeCount();
62 if(!numEpisodes) return false;
63
64 // The number of episodes determines how to interpret the arguments.
65 /// @note Logic here aims to be somewhat vanilla compatible, yet offer
66 /// a limited degree of support for custom episodes. The "playmusic"
67 /// cmd is a far more flexible method of changing music.
68 String episodeId;
69 int warpNumber;
70 if(numEpisodes > 1)
71 {
72 episodeId = String::number(args[0] - '0');
73 warpNumber = args[1] - '0';
74 }
75 else
76 {
77 episodeId = FirstPlayableEpisodeId();
78 warpNumber = (args[0] - '0') * 10 + (args[1] - '0');
79 }
80
81 // Lookup and try to enqueue the Music for the referenced episode and map.
82 const auto mapUri = TranslateMapWarpNumber(episodeId, warpNumber);
83 if (S_MapMusic(mapUri))
84 {
85 P_SetMessageWithFlags(plr, STSTR_MUS, LMF_NO_HIDE);
86 return true;
87 }
88
89 P_SetMessageWithFlags(plr, STSTR_NOMUS, LMF_NO_HIDE);
90 return false;
91 }
92
CHEAT_FUNC(Reveal)93 CHEAT_FUNC(Reveal)
94 {
95 DENG2_UNUSED2(args, numArgs);
96
97 if(IS_NETGAME && gfw_Rule(deathmatch))
98 return false;
99
100 if(player < 0 || player >= MAXPLAYERS)
101 return false;
102
103 player_t *plr = &players[player];
104
105 // Dead players can't cheat.
106 if(plr->health <= 0) return false;
107
108 if(ST_AutomapIsOpen(player))
109 {
110 ST_CycleAutomapCheatLevel(player);
111 }
112 return true;
113 }
114
CHEAT_FUNC(Powerup)115 CHEAT_FUNC(Powerup)
116 {
117 DENG2_UNUSED2(args, numArgs);
118 if(player < 0 || player >= MAXPLAYERS)
119 return false;
120
121 P_SetMessageWithFlags(&players[player], STSTR_BEHOLD, LMF_NO_HIDE);
122 return true;
123 }
124
CHEAT_FUNC(Powerup2)125 CHEAT_FUNC(Powerup2)
126 {
127 DENG2_UNUSED(numArgs);
128 if(player < 0 || player >= MAXPLAYERS)
129 return false;
130
131 struct mnemonic_pair_s {
132 char vanilla;
133 char give;
134 } static const mnemonics[] =
135 {
136 /*PT_INVULNERABILITY*/ { 'v', 'i' },
137 /*PT_STRENGTH*/ { 's', 'b' },
138 /*PT_INVISIBILITY*/ { 'i', 'v' },
139 /*PT_IRONFEET*/ { 'r', 's' },
140 /*PT_ALLMAP*/ { 'a', 'm' },
141 /*PT_INFRARED*/ { 'l', 'g' }
142 };
143 static int const numMnemonics = int(sizeof(mnemonics) / sizeof(mnemonics[0]));
144
145 for(int i = 0; i < numMnemonics; ++i)
146 {
147 if(args[0] == mnemonics[i].vanilla)
148 {
149 DD_Executef(true, "give %c %i", mnemonics[i].give, player);
150 return true;
151 }
152 }
153 return false;
154 }
155
CHEAT_FUNC(MyPos)156 CHEAT_FUNC(MyPos)
157 {
158 DENG2_UNUSED2(args, numArgs);
159 if(player < 0 || player >= MAXPLAYERS)
160 return false;
161
162 mobj_t const *mob = players[CONSOLEPLAYER].plr->mo;
163 String const text = String("angle:0x%1 position:%2")
164 .arg(mob->angle, 0, 16)
165 .arg(Vector3d(mob->origin).asText());
166 P_SetMessageWithFlags(&players[player], text.toUtf8().constData(), LMF_NO_HIDE);
167 return true;
168 }
169
170 /**
171 * The multipurpose cheat ccmd.
172 */
D_CMD(Cheat)173 D_CMD(Cheat)
174 {
175 DENG2_UNUSED2(src, argc);
176
177 // Give each of the characters in argument two to the ST event handler.
178 int const len = qstrlen(argv[1]);
179 for(int i = 0; i < len; ++i)
180 {
181 event_t ev; de::zap(ev);
182 ev.type = EV_KEY;
183 ev.state = EVS_DOWN;
184 ev.data1 = argv[1][i];
185 ev.data2 = ev.data3 = 0;
186 G_EventSequenceResponder(&ev);
187 }
188 return true;
189 }
190
D_CMD(CheatGod)191 D_CMD(CheatGod)
192 {
193 DENG2_UNUSED(src);
194
195 if(G_GameState() == GS_MAP)
196 {
197 if(IS_CLIENT)
198 {
199 NetCl_CheatRequest("god");
200 }
201 else if((IS_NETGAME && !netSvAllowCheats) ||
202 gfw_Rule(skill) == SM_NIGHTMARE)
203 {
204 return false;
205 }
206 else
207 {
208 int player = CONSOLEPLAYER;
209 if(argc == 2)
210 {
211 player = String(argv[1]).toInt();
212 if(player < 0 || player >= MAXPLAYERS) return false;
213 }
214
215 player_t *plr = &players[player];
216 if(!plr->plr->inGame) return false;
217
218 // Dead players can't cheat.
219 if(plr->health <= 0) return false;
220
221 plr->cheats ^= CF_GODMODE;
222 plr->update |= PSF_STATE;
223
224 if(P_GetPlayerCheats(plr) & CF_GODMODE)
225 {
226 if(plr->plr->mo)
227 plr->plr->mo->health = maxHealth;
228 plr->health = godModeHealth;
229 plr->update |= PSF_HEALTH;
230 }
231
232 P_SetMessageWithFlags(plr, ((P_GetPlayerCheats(plr) & CF_GODMODE) ? STSTR_DQDON : STSTR_DQDOFF), LMF_NO_HIDE);
233 }
234 }
235 return true;
236 }
237
D_CMD(CheatNoClip)238 D_CMD(CheatNoClip)
239 {
240 DENG2_UNUSED(src);
241
242 if(G_GameState() == GS_MAP)
243 {
244 if(IS_CLIENT)
245 {
246 NetCl_CheatRequest("noclip");
247 }
248 else if((IS_NETGAME && !netSvAllowCheats) ||
249 gfw_Rule(skill) == SM_NIGHTMARE)
250 {
251 return false;
252 }
253 else
254 {
255 int player = CONSOLEPLAYER;
256 if(argc == 2)
257 {
258 player = String(argv[1]).toInt();
259 if(player < 0 || player >= MAXPLAYERS) return false;
260 }
261
262 player_t *plr = &players[player];
263 if(!plr->plr->inGame) return false;
264
265 // Dead players can't cheat.
266 if(plr->health <= 0) return false;
267
268 plr->cheats ^= CF_NOCLIP;
269 plr->update |= PSF_STATE;
270 P_SetMessageWithFlags(plr, ((P_GetPlayerCheats(plr) & CF_NOCLIP) ? STSTR_NCON : STSTR_NCOFF), LMF_NO_HIDE);
271 }
272 }
273 return true;
274 }
275
suicideResponse(msgresponse_t response,int,void *)276 static int suicideResponse(msgresponse_t response, int /*userValue*/, void * /*context*/)
277 {
278 if(response == MSG_YES)
279 {
280 if(IS_NETGAME && IS_CLIENT)
281 {
282 NetCl_CheatRequest("suicide");
283 }
284 else
285 {
286 player_t *plr = &players[CONSOLEPLAYER];
287 P_DamageMobj(plr->plr->mo, nullptr, nullptr, 10000, false);
288 }
289 }
290 return true;
291 }
292
D_CMD(CheatSuicide)293 D_CMD(CheatSuicide)
294 {
295 DENG2_UNUSED(src);
296
297 if(G_GameState() == GS_MAP)
298 {
299 int player = CONSOLEPLAYER;
300 if(!IS_CLIENT || argc == 2)
301 {
302 player = String(argv[1]).toInt();
303 if(player < 0 || player >= MAXPLAYERS) return false;
304 }
305
306 player_t *plr = &players[player];
307 if(!plr->plr->inGame) return false;
308 if(plr->playerState == PST_DEAD) return false;
309
310 if(!IS_NETGAME || IS_CLIENT)
311 {
312 Hu_MsgStart(MSG_YESNO, SUICIDEASK, suicideResponse, 0, nullptr);
313 }
314 else
315 {
316 P_DamageMobj(plr->plr->mo, nullptr, nullptr, 10000, false);
317 }
318 return true;
319 }
320
321 Hu_MsgStart(MSG_ANYKEY, SUICIDEOUTMAP, nullptr, 0, nullptr);
322 return true;
323 }
324
D_CMD(CheatReveal)325 D_CMD(CheatReveal)
326 {
327 DENG2_UNUSED2(src, argc);
328 // Server operator can always reveal.
329 if(IS_NETGAME && !IS_NETWORK_SERVER)
330 return false;
331
332 int const option = String(argv[1]).toInt();
333 if(option < 0 || option > 3) return false;
334
335 for(int i = 0; i < MAXPLAYERS; ++i)
336 {
337 ST_SetAutomapCheatLevel(i, 0);
338 ST_RevealAutomap(i, false);
339 if(option == 1)
340 {
341 ST_RevealAutomap(i, true);
342 }
343 else if(option != 0)
344 {
345 ST_SetAutomapCheatLevel(i, option - 1);
346 }
347 }
348
349 return true;
350 }
351
giveWeapon(player_t * player,weapontype_t weaponType)352 static void giveWeapon(player_t *player, weapontype_t weaponType)
353 {
354 P_GiveWeapon(player, weaponType, false/*not dropped*/);
355 if(weaponType == WT_EIGHTH)
356 {
357 P_SetMessageWithFlags(player, STSTR_CHOPPERS, LMF_NO_HIDE);
358 }
359 }
360
togglePower(player_t * player,powertype_t powerType)361 static void togglePower(player_t *player, powertype_t powerType)
362 {
363 P_TogglePower(player, powerType);
364 P_SetMessageWithFlags(player, STSTR_BEHOLDX, LMF_NO_HIDE);
365 }
366
D_CMD(CheatGive)367 D_CMD(CheatGive)
368 {
369 DENG2_UNUSED(src);
370
371 if(G_GameState() != GS_MAP)
372 {
373 LOG_SCR_ERROR("Can only \"give\" when in a game!");
374 return true;
375 }
376
377 if(argc != 2 && argc != 3)
378 {
379 LOG_SCR_NOTE("Usage:\n give (stuff)\n give (stuff) (player number)");
380
381 #define TABBED(A, B) "\n" _E(Ta) _E(b) " " << A << " " _E(.) _E(Tb) << B
382 LOG_SCR_MSG("Where (stuff) is one or more type:id codes"
383 " (if no id, give all of that type):")
384 << TABBED("a", "Ammo")
385 << TABBED("b", "Berserk")
386 << TABBED("f", "Flight ability")
387 << TABBED("g", "Light amplification visor")
388 << TABBED("h", "Health")
389 << TABBED("i", "Invulnerability")
390 << TABBED("k", "Keys")
391 << TABBED("m", "Computer area map")
392 << TABBED("p", "Backpack full of ammo")
393 << TABBED("r", "Armor")
394 << TABBED("s", "Radiation shielding suit")
395 << TABBED("v", "Invisibility")
396 << TABBED("w", "Weapons");
397 #undef TABBED
398
399 LOG_SCR_MSG(_E(D) "Examples:");
400 LOG_SCR_MSG(" " _E(>) "Enter " _E(b) "give arw" _E(.) " for full ammo and armor " _E(l) "(equivalent to cheat IDFA)");
401 LOG_SCR_MSG(" " _E(>) "Enter " _E(b) "give w2k1" _E(.) " for weapon two and key one");
402 return true;
403 }
404
405 int player = CONSOLEPLAYER;
406 if(argc == 3)
407 {
408 player = String(argv[2]).toInt();
409 if(player < 0 || player >= MAXPLAYERS) return false;
410 }
411
412 if(IS_CLIENT)
413 {
414 if(argc < 2) return false;
415
416 String const request = String("give ") + argv[1];
417 NetCl_CheatRequest(request.toUtf8().constData());
418 return true;
419 }
420
421 if(IS_NETGAME && !netSvAllowCheats) return false;
422 if(gfw_Rule(skill) == SM_NIGHTMARE) return false;
423
424 player_t *plr = &players[player];
425
426 // Can't give to a player who's not in the game.
427 if(!plr->plr->inGame) return false;
428 // Can't give to a dead player.
429 if(plr->health <= 0) return false;
430
431 String const stuff = String(argv[1]).toLower(); // Stuff is the 2nd arg.
432 for(int i = 0; i < stuff.length(); ++i)
433 {
434 QChar const mnemonic = stuff.at(i);
435 switch(mnemonic.toLatin1())
436 {
437 // Ammo:
438 case 'a': {
439 ammotype_t ammos = NUM_AMMO_TYPES; // All types.
440
441 // Give one specific ammo type?
442 if((i + 1) < stuff.length() && stuff.at(i + 1).isDigit())
443 {
444 int const arg = stuff.at( ++i ).digitValue();
445 if(arg < AT_FIRST || arg >= NUM_AMMO_TYPES)
446 {
447 LOG_SCR_ERROR("Ammo #%d unknown. Valid range %s")
448 << arg << Rangei(AT_FIRST, NUM_AMMO_TYPES).asText();
449 break;
450 }
451 ammos = ammotype_t(arg);
452 }
453 P_GiveAmmo(plr, ammos, -1 /*max rounds*/);
454 break; }
455
456 // Armor:
457 case 'r': {
458 int armor = 1;
459
460 if((i + 1) < stuff.length() && stuff.at(i + 1).isDigit())
461 {
462 int const arg = stuff.at( ++i ).digitValue();
463 if(arg < 0 || arg >= 4)
464 {
465 LOG_SCR_ERROR("Armor #%d unknown. Valid range %s")
466 << arg << Rangei(0, 4).asText();
467 break;
468 }
469 armor = arg;
470 }
471 P_GiveArmor(plr, armorClass[armor], armorPoints[armor]);
472 break; }
473
474 // Keys:
475 case 'k': {
476 keytype_t keys = NUM_KEY_TYPES; // All types.
477
478 // Give one specific key type?
479 if((i + 1) < stuff.length() && stuff.at(i + 1).isDigit())
480 {
481 int const arg = stuff.at( ++i ).digitValue();
482 if(arg < KT_FIRST || arg >= NUM_KEY_TYPES)
483 {
484 LOG_SCR_ERROR("Key #%d unknown. Valid range %s")
485 << arg << Rangei(KT_FIRST, NUM_KEY_TYPES).asText();
486 break;
487 }
488 keys = keytype_t(arg);
489 }
490 P_GiveKey(plr, keys);
491 break; }
492
493 // Misc:
494 case 'p': P_GiveBackpack(plr); break;
495 case 'h': P_GiveHealth(plr, healthLimit); break;
496
497 // Powers:
498 case 'm': togglePower(plr, PT_ALLMAP); break;
499 case 'f': togglePower(plr, PT_FLIGHT); break;
500 case 'g': togglePower(plr, PT_INFRARED); break;
501 case 'v': togglePower(plr, PT_INVISIBILITY); break;
502 case 'i': togglePower(plr, PT_INVULNERABILITY); break;
503 case 's': togglePower(plr, PT_IRONFEET); break;
504 case 'b': togglePower(plr, PT_STRENGTH); break;
505
506 // Weapons:
507 case 'w': {
508 weapontype_t weapons = NUM_WEAPON_TYPES; // All types.
509
510 // Give one specific weapon type?
511 if((i + 1) < stuff.length() && stuff.at(i + 1).isDigit())
512 {
513 int const arg = stuff.at( ++i ).digitValue();
514 if(arg < WT_FIRST || arg >= NUM_WEAPON_TYPES)
515 {
516 LOG_SCR_ERROR("Weapon #%d unknown. Valid range %s")
517 << arg << Rangei(WT_FIRST, NUM_WEAPON_TYPES).asText();
518 break;
519 }
520 weapons = weapontype_t(arg);
521 }
522 giveWeapon(plr, weapons);
523 break; }
524
525 default: // Unrecognized.
526 LOG_SCR_ERROR("Mnemonic '%c' unknown, cannot give") << mnemonic.toLatin1();
527 break;
528 }
529 }
530
531 // If the give expression matches that of a vanilla cheat code print the
532 // associated confirmation message to the player's log.
533 /// @todo fixme: Somewhat of kludge...
534 if(stuff == "war2")
535 {
536 P_SetMessageWithFlags(plr, STSTR_FAADDED, LMF_NO_HIDE);
537 }
538 else if(stuff == "wakr3")
539 {
540 P_SetMessageWithFlags(plr, STSTR_KFAADDED, LMF_NO_HIDE);
541 }
542
543 return true;
544 }
545
D_CMD(CheatMassacre)546 D_CMD(CheatMassacre)
547 {
548 DENG2_UNUSED3(src, argc, argv);
549
550 if(G_GameState() == GS_MAP)
551 {
552 if(IS_CLIENT)
553 {
554 NetCl_CheatRequest("kill");
555 }
556 else if((IS_NETGAME && !netSvAllowCheats) ||
557 gfw_Rule(skill) == SM_NIGHTMARE)
558 {
559 return false;
560 }
561 else
562 {
563 int const killCount = P_Massacre();
564 LOG_SCR_MSG("%i monsters killed") << killCount;
565 }
566 }
567 return true;
568 }
569
D_CMD(CheatWhere)570 D_CMD(CheatWhere)
571 {
572 DENG2_UNUSED3(src, argc, argv);
573
574 if(G_GameState() != GS_MAP)
575 return true;
576
577 player_t *plr = &players[CONSOLEPLAYER];
578 mobj_t *plrMo = plr->plr->mo;
579 if(!plrMo) return true;
580
581 String const text = String("Map:%1 position:%2")
582 .arg(gfw_Session()->mapUri().path().asText())
583 .arg(Vector3d(plrMo->origin).asText());
584 P_SetMessageWithFlags(plr, text.toUtf8().constData(), LMF_NO_HIDE);
585
586 // Also print the some information to the console.
587 LOG_SCR_NOTE("%s") << text;
588
589 Sector *sector = Mobj_Sector(plrMo);
590
591 uri_s *matUri = Materials_ComposeUri(P_GetIntp(sector, DMU_FLOOR_MATERIAL));
592 LOG_SCR_MSG("FloorZ:%f Material:%s")
593 << P_GetDoublep(sector, DMU_FLOOR_HEIGHT)
594 << Str_Text(Uri_ToString(matUri));
595 Uri_Delete(matUri);
596
597 matUri = Materials_ComposeUri(P_GetIntp(sector, DMU_CEILING_MATERIAL));
598 LOG_SCR_MSG("CeilingZ:%f Material:%s")
599 << P_GetDoublep(sector, DMU_CEILING_HEIGHT)
600 << Str_Text(Uri_ToString(matUri));
601 Uri_Delete(matUri);
602
603 LOG_SCR_MSG("Player height:%f Player radius:%f")
604 << plrMo->height << plrMo->radius;
605
606 return true;
607 }
608
G_RegisterCheats()609 void G_RegisterCheats()
610 {
611 /// Helper macro for registering new cheat event sequence handlers.
612 #define ADDCHEAT(name, callback) G_AddEventSequence((name), CHEAT(callback))
613
614 /// Helper macro for registering new cheat event sequence command handlers.
615 #define ADDCHEATCMD(name, cmdTemplate) G_AddEventSequenceCommand((name), cmdTemplate)
616
617 switch(gameMode)
618 {
619 case doom2_hacx:
620 ADDCHEATCMD("blast", "give wakr3 %p");
621 ADDCHEATCMD("boots", "give s %p");
622 ADDCHEATCMD("bright", "give g %p");
623 ADDCHEATCMD("ghost", "give v %p");
624 ADDCHEAT ("seeit%1", Powerup2);
625 ADDCHEAT ("seeit", Powerup);
626 ADDCHEAT ("show", Reveal);
627 ADDCHEATCMD("superman", "give i %p");
628 ADDCHEAT ("tunes%1%2", Music);
629 ADDCHEATCMD("walk", "noclip %p");
630 ADDCHEATCMD("warpme%1%2", "warp %1%2");
631 ADDCHEATCMD("whacko", "give b %p");
632 ADDCHEAT ("wheream", MyPos);
633 ADDCHEATCMD("wuss", "god %p");
634 ADDCHEATCMD("zap", "give w7 %p");
635 break;
636
637 case doom_chex:
638 ADDCHEATCMD("allen", "give s %p");
639 ADDCHEATCMD("andrewbenson", "give i %p");
640 ADDCHEATCMD("charlesjacobi", "noclip %p");
641 ADDCHEATCMD("davidbrus", "god %p");
642 ADDCHEATCMD("deanhyers", "give b %p");
643 ADDCHEATCMD("digitalcafe", "give m %p");
644 ADDCHEAT ("idmus%1%2", Music);
645 ADDCHEATCMD("joelkoenigs", "give w7 %p");
646 ADDCHEATCMD("joshuastorms", "give g %p");
647 ADDCHEAT ("kimhyers", MyPos);
648 ADDCHEATCMD("leesnyder%1%2", "warp %1 %2");
649 ADDCHEATCMD("marybregi", "give v %p");
650 ADDCHEATCMD("mikekoenigs", "give war2 %p");
651 ADDCHEATCMD("scottholman", "give wakr3 %p");
652 ADDCHEAT ("sherrill", Reveal);
653 break;
654
655 default: // Doom
656 ADDCHEAT ("idbehold%1", Powerup2);
657 ADDCHEAT ("idbehold", Powerup);
658
659 // Note that in vanilla this cheat enables invulnerability until the
660 // end of the current tic.
661 ADDCHEATCMD("idchoppers", "give w7 %p");
662
663 ADDCHEATCMD("idclev%1%2", ((gameModeBits & GM_ANY_DOOM)? "warp %1 %2" : "warp %1%2"));
664 ADDCHEATCMD("idclip", "noclip %p");
665 ADDCHEATCMD("iddqd", "god %p");
666 ADDCHEAT ("iddt", Reveal);
667 ADDCHEATCMD("idfa", "give war2 %p");
668 ADDCHEATCMD("idkfa", "give wakr3 %p");
669 ADDCHEAT ("idmus%1%2", Music);
670 ADDCHEAT ("idmypos", MyPos);
671 ADDCHEATCMD("idspispopd", "noclip %p");
672 break;
673 }
674
675 #undef ADDCHEATCMD
676 #undef ADDCHEAT
677 }
678