mirror of
https://github.com/ZDoom/qzdoom-gpl.git
synced 2024-12-13 21:51:08 +00:00
a1a0bdefff
Patches from DeHackEd 2.3 and 2.4 are now loaded without warnings/errors
3205 lines
80 KiB
C++
3205 lines
80 KiB
C++
/*
|
|
** d_dehacked.cpp
|
|
** Parses dehacked/bex patches and changes game structures accordingly
|
|
**
|
|
**---------------------------------------------------------------------------
|
|
** Copyright 1998-2006 Randy Heit
|
|
** All rights reserved.
|
|
**
|
|
** Redistribution and use in source and binary forms, with or without
|
|
** modification, are permitted provided that the following conditions
|
|
** are met:
|
|
**
|
|
** 1. Redistributions of source code must retain the above copyright
|
|
** notice, this list of conditions and the following disclaimer.
|
|
** 2. Redistributions in binary form must reproduce the above copyright
|
|
** notice, this list of conditions and the following disclaimer in the
|
|
** documentation and/or other materials provided with the distribution.
|
|
** 3. The name of the author may not be used to endorse or promote products
|
|
** derived from this software without specific prior written permission.
|
|
**
|
|
** THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
|
|
** IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
|
** OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
|
** IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
|
|
** INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
|
|
** NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
|
** THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
**---------------------------------------------------------------------------
|
|
**
|
|
** Much of this file is fudging code to compensate for the fact that most of
|
|
** what could be changed with Dehacked is no longer in the same state it was
|
|
** in as of Doom 1.9. There is a lump in zdoom.wad (DEHSUPP) that stores most
|
|
** of the lookup tables used so that they need not be loaded all the time.
|
|
** Also, their total size is less in the lump than when they were part of the
|
|
** executable, so it saves space on disk as well as in memory.
|
|
*/
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <stddef.h>
|
|
|
|
#include "doomtype.h"
|
|
#include "templates.h"
|
|
#include "doomstat.h"
|
|
#include "info.h"
|
|
#include "d_dehacked.h"
|
|
#include "s_sound.h"
|
|
#include "g_level.h"
|
|
#include "cmdlib.h"
|
|
#include "gstrings.h"
|
|
#include "m_alloc.h"
|
|
#include "m_misc.h"
|
|
#include "w_wad.h"
|
|
#include "d_player.h"
|
|
#include "r_state.h"
|
|
#include "gi.h"
|
|
#include "c_dispatch.h"
|
|
#include "decallib.h"
|
|
#include "v_palette.h"
|
|
#include "a_sharedglobal.h"
|
|
#include "vectors.h"
|
|
#include "dobject.h"
|
|
#include "r_data/r_translate.h"
|
|
#include "sc_man.h"
|
|
#include "i_system.h"
|
|
#include "doomerrors.h"
|
|
#include "p_effect.h"
|
|
#include "farchive.h"
|
|
#include "vmbuilder.h"
|
|
|
|
// [SO] Just the way Randy said to do it :)
|
|
// [RH] Made this CVAR_SERVERINFO
|
|
CVAR (Int, infighting, 0, CVAR_SERVERINFO)
|
|
|
|
static bool LoadDehSupp ();
|
|
static void UnloadDehSupp ();
|
|
|
|
|
|
// This is a list of all the action functions used by each of Doom's states.
|
|
static TArray<PFunction *> Actions;
|
|
|
|
// These are the original heights of every Doom 2 thing. They are used if a patch
|
|
// specifies that a thing should be hanging from the ceiling but doesn't specify
|
|
// a height for the thing, since these are the heights it probably wants.
|
|
static TArray<int> OrgHeights;
|
|
|
|
// DeHackEd made the erroneous assumption that if a state didn't appear in
|
|
// Doom with an action function, then it was incorrect to assign it one.
|
|
// This is a list of the states that had action functions, so we can figure
|
|
// out where in the original list of states a DeHackEd codepointer is.
|
|
// (DeHackEd might also have done this for compatibility between Doom
|
|
// versions, because states could move around, but actions would never
|
|
// disappear, but that doesn't explain why frame patches specify an exact
|
|
// state rather than a code pointer.)
|
|
static TArray<int> CodePConv;
|
|
|
|
// Sprite names in the order Doom originally had them.
|
|
struct DEHSprName
|
|
{
|
|
char c[5];
|
|
};
|
|
static TArray<DEHSprName> OrgSprNames;
|
|
|
|
struct StateMapper
|
|
{
|
|
FState *State;
|
|
int StateSpan;
|
|
PClassActor *Owner;
|
|
bool OwnerIsPickup;
|
|
};
|
|
|
|
static TArray<StateMapper> StateMap;
|
|
|
|
// Sound equivalences. When a patch tries to change a sound,
|
|
// use these sound names.
|
|
static TArray<FSoundID> SoundMap;
|
|
|
|
// Names of different actor types, in original Doom 2 order
|
|
static TArray<PClassActor *> InfoNames;
|
|
|
|
// bit flags for PatchThing (a .bex extension):
|
|
struct BitName
|
|
{
|
|
char Name[20];
|
|
BYTE Bit;
|
|
BYTE WhichFlags;
|
|
};
|
|
|
|
static TArray<BitName> BitNames;
|
|
|
|
// Render styles
|
|
struct StyleName
|
|
{
|
|
char Name[20];
|
|
BYTE Num;
|
|
};
|
|
|
|
static TArray<StyleName> StyleNames;
|
|
|
|
static TArray<PClassAmmo *> AmmoNames;
|
|
static TArray<PClassActor *> WeaponNames;
|
|
|
|
// DeHackEd trickery to support MBF-style parameters
|
|
// List of states that are hacked to use a codepointer
|
|
struct MBFParamState
|
|
{
|
|
FState *state;
|
|
int pointer;
|
|
};
|
|
static TArray<MBFParamState> MBFParamStates;
|
|
// Data on how to correctly modify the codepointers
|
|
struct CodePointerAlias
|
|
{
|
|
FName name;
|
|
char alias[20];
|
|
BYTE params;
|
|
};
|
|
static TArray<CodePointerAlias> MBFCodePointers;
|
|
|
|
struct AmmoPerAttack
|
|
{
|
|
VMNativeFunction **func;
|
|
int ammocount;
|
|
};
|
|
|
|
DECLARE_ACTION(A_Punch)
|
|
DECLARE_ACTION(A_FirePistol)
|
|
DECLARE_ACTION(A_FireShotgun)
|
|
DECLARE_ACTION(A_FireShotgun2)
|
|
DECLARE_ACTION(A_FireCGun)
|
|
DECLARE_ACTION(A_FireMissile)
|
|
DECLARE_ACTION(A_Saw)
|
|
DECLARE_ACTION(A_FirePlasma)
|
|
DECLARE_ACTION(A_FireBFG)
|
|
DECLARE_ACTION(A_FireOldBFG)
|
|
DECLARE_ACTION(A_FireRailgun)
|
|
|
|
// Default ammo use of the various weapon attacks
|
|
static AmmoPerAttack AmmoPerAttacks[] = {
|
|
{ &A_Punch_VMPtr, 0},
|
|
{ &A_FirePistol_VMPtr, 1},
|
|
{ &A_FireShotgun_VMPtr, 1},
|
|
{ &A_FireShotgun2_VMPtr, 2},
|
|
{ &A_FireCGun_VMPtr, 1},
|
|
{ &A_FireMissile_VMPtr, 1},
|
|
{ &A_Saw_VMPtr, 0},
|
|
{ &A_FirePlasma_VMPtr, 1},
|
|
{ &A_FireBFG_VMPtr, -1}, // uses deh.BFGCells
|
|
{ &A_FireOldBFG_VMPtr, 1},
|
|
{ &A_FireRailgun_VMPtr, 1},
|
|
{ NULL, 0}
|
|
};
|
|
|
|
|
|
// Miscellaneous info that used to be constant
|
|
DehInfo deh =
|
|
{
|
|
100, // .StartHealth
|
|
50, // .StartBullets
|
|
100, // .MaxHealth
|
|
200, // .MaxArmor
|
|
1, // .GreenAC
|
|
2, // .BlueAC
|
|
200, // .MaxSoulsphere
|
|
100, // .SoulsphereHealth
|
|
200, // .MegasphereHealth
|
|
100, // .GodHealth
|
|
200, // .FAArmor
|
|
2, // .FAAC
|
|
200, // .KFAArmor
|
|
2, // .KFAAC
|
|
"PLAY", // Name of player sprite
|
|
255, // Rocket explosion style, 255=use cvar
|
|
2./3., // Rocket explosion alpha
|
|
false, // .NoAutofreeze
|
|
40, // BFG cells per shot
|
|
};
|
|
|
|
// Doom identified pickup items by their sprites. ZDoom prefers to use their
|
|
// class type to identify them instead. To support the traditional Doom
|
|
// behavior, for every thing touched by dehacked that has the MF_PICKUP flag,
|
|
// a new subclass of ADehackedPickup will be created with properties copied
|
|
// from the original actor's defaults. The original actor is then changed to
|
|
// spawn the new class.
|
|
|
|
IMPLEMENT_POINTY_CLASS (ADehackedPickup)
|
|
DECLARE_POINTER (RealPickup)
|
|
END_POINTERS
|
|
|
|
TArray<PClassActor *> TouchedActors;
|
|
|
|
char *UnchangedSpriteNames;
|
|
int NumUnchangedSprites;
|
|
|
|
// Sprite<->Class map for ADehackedPickup::DetermineType
|
|
static struct DehSpriteMap
|
|
{
|
|
char Sprite[5];
|
|
const char *ClassName;
|
|
}
|
|
DehSpriteMappings[] =
|
|
{
|
|
{ "AMMO", "ClipBox" },
|
|
{ "ARM1", "GreenArmor" },
|
|
{ "ARM2", "BlueArmor" },
|
|
{ "BFUG", "BFG9000" },
|
|
{ "BKEY", "BlueCard" },
|
|
{ "BON1", "HealthBonus" },
|
|
{ "BON2", "ArmorBonus" },
|
|
{ "BPAK", "Backpack" },
|
|
{ "BROK", "RocketBox" },
|
|
{ "BSKU", "BlueSkull" },
|
|
{ "CELL", "Cell" },
|
|
{ "CELP", "CellPack" },
|
|
{ "CLIP", "Clip" },
|
|
{ "CSAW", "Chainsaw" },
|
|
{ "LAUN", "RocketLauncher" },
|
|
{ "MEDI", "Medikit" },
|
|
{ "MEGA", "Megasphere" },
|
|
{ "MGUN", "Chaingun" },
|
|
{ "PINS", "BlurSphere" },
|
|
{ "PINV", "InvulnerabilitySphere" },
|
|
{ "PLAS", "PlasmaRifle" },
|
|
{ "PMAP", "Allmap" },
|
|
{ "PSTR", "Berserk" },
|
|
{ "PVIS", "Infrared" },
|
|
{ "RKEY", "RedCard" },
|
|
{ "ROCK", "RocketAmmo" },
|
|
{ "RSKU", "RedSkull" },
|
|
{ "SBOX", "ShellBox" },
|
|
{ "SGN2", "SuperShotgun" },
|
|
{ "SHEL", "Shell" },
|
|
{ "SHOT", "Shotgun" },
|
|
{ "SOUL", "Soulsphere" },
|
|
{ "STIM", "Stimpack" },
|
|
{ "SUIT", "RadSuit" },
|
|
{ "YKEY", "YellowCard" },
|
|
{ "YSKU", "YellowSkull" }
|
|
};
|
|
|
|
#define LINESIZE 2048
|
|
|
|
#define CHECKKEY(a,b) if (!stricmp (Line1, (a))) (b) = atoi(Line2);
|
|
|
|
static char *PatchFile, *PatchPt, *PatchName;
|
|
static int PatchSize;
|
|
static char *Line1, *Line2;
|
|
static int dversion, pversion;
|
|
static bool including, includenotext;
|
|
|
|
static const char *unknown_str = "Unknown key %s encountered in %s %d.\n";
|
|
|
|
static FStringTable *EnglishStrings;
|
|
|
|
// This is an offset to be used for computing the text stuff.
|
|
// Straight from the DeHackEd source which was
|
|
// Written by Greg Lewis, gregl@umich.edu.
|
|
static int toff[] = {129044, 129044, 129044, 129284, 129380};
|
|
|
|
struct Key {
|
|
const char *name;
|
|
ptrdiff_t offset;
|
|
};
|
|
|
|
static int PatchThing (int);
|
|
static int PatchSound (int);
|
|
static int PatchFrame (int);
|
|
static int PatchSprite (int);
|
|
static int PatchAmmo (int);
|
|
static int PatchWeapon (int);
|
|
static int PatchPointer (int);
|
|
static int PatchCheats (int);
|
|
static int PatchMisc (int);
|
|
static int PatchText (int);
|
|
static int PatchStrings (int);
|
|
static int PatchPars (int);
|
|
static int PatchCodePtrs (int);
|
|
static int PatchMusic (int);
|
|
static int DoInclude (int);
|
|
static bool DoDehPatch();
|
|
|
|
static const struct {
|
|
const char *name;
|
|
int (*func)(int);
|
|
} Modes[] = {
|
|
// These appear in .deh and .bex files
|
|
{ "Thing", PatchThing },
|
|
{ "Sound", PatchSound },
|
|
{ "Frame", PatchFrame },
|
|
{ "Sprite", PatchSprite },
|
|
{ "Ammo", PatchAmmo },
|
|
{ "Weapon", PatchWeapon },
|
|
{ "Pointer", PatchPointer },
|
|
{ "Cheat", PatchCheats },
|
|
{ "Misc", PatchMisc },
|
|
{ "Text", PatchText },
|
|
// These appear in .bex files
|
|
{ "include", DoInclude },
|
|
{ "[STRINGS]", PatchStrings },
|
|
{ "[PARS]", PatchPars },
|
|
{ "[CODEPTR]", PatchCodePtrs },
|
|
{ "[MUSIC]", PatchMusic },
|
|
{ NULL, NULL },
|
|
};
|
|
|
|
static int HandleMode (const char *mode, int num);
|
|
static bool HandleKey (const struct Key *keys, void *structure, const char *key, int value);
|
|
static bool ReadChars (char **stuff, int size);
|
|
static char *igets (void);
|
|
static int GetLine (void);
|
|
|
|
inline double DEHToDouble(int acsval)
|
|
{
|
|
return acsval / 65536.;
|
|
}
|
|
|
|
static void PushTouchedActor(PClassActor *cls)
|
|
{
|
|
for(unsigned i = 0; i < TouchedActors.Size(); i++)
|
|
{
|
|
if (TouchedActors[i] == cls)
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
TouchedActors.Push(cls);
|
|
}
|
|
|
|
|
|
static int HandleMode (const char *mode, int num)
|
|
{
|
|
int i = 0;
|
|
while (Modes[i].name && stricmp (Modes[i].name, mode))
|
|
i++;
|
|
|
|
if (Modes[i].name)
|
|
return Modes[i].func (num);
|
|
|
|
// Handle unknown or unimplemented data
|
|
Printf ("Unknown chunk %s encountered. Skipping.\n", mode);
|
|
do
|
|
i = GetLine ();
|
|
while (i == 1);
|
|
|
|
return i;
|
|
}
|
|
|
|
static bool HandleKey (const struct Key *keys, void *structure, const char *key, int value)
|
|
{
|
|
while (keys->name && stricmp (keys->name, key))
|
|
keys++;
|
|
|
|
if (keys->name) {
|
|
*((int *)(((BYTE *)structure) + keys->offset)) = value;
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static int FindSprite (const char *sprname)
|
|
{
|
|
int i;
|
|
DWORD nameint = *((DWORD *)sprname);
|
|
|
|
for (i = 0; i < NumUnchangedSprites; ++i)
|
|
{
|
|
if (*((DWORD *)&UnchangedSpriteNames[i*4]) == nameint)
|
|
{
|
|
return i;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
static FState *FindState (int statenum)
|
|
{
|
|
int stateacc;
|
|
unsigned i;
|
|
|
|
if (statenum == 0)
|
|
return NULL;
|
|
|
|
for (i = 0, stateacc = 1; i < StateMap.Size(); i++)
|
|
{
|
|
if (stateacc <= statenum && stateacc + StateMap[i].StateSpan > statenum)
|
|
{
|
|
if (StateMap[i].State != NULL)
|
|
{
|
|
if (StateMap[i].OwnerIsPickup)
|
|
{
|
|
PushTouchedActor(StateMap[i].Owner);
|
|
}
|
|
return StateMap[i].State + statenum - stateacc;
|
|
}
|
|
else return NULL;
|
|
}
|
|
stateacc += StateMap[i].StateSpan;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
int FindStyle (const char *namestr)
|
|
{
|
|
for(unsigned i = 0; i < StyleNames.Size(); i++)
|
|
{
|
|
if (!stricmp(StyleNames[i].Name, namestr)) return StyleNames[i].Num;
|
|
}
|
|
DPrintf("Unknown render style %s\n", namestr);
|
|
return -1;
|
|
}
|
|
|
|
static bool ReadChars (char **stuff, int size)
|
|
{
|
|
char *str = *stuff;
|
|
|
|
if (!size) {
|
|
*str = 0;
|
|
return true;
|
|
}
|
|
|
|
do {
|
|
// Ignore carriage returns
|
|
if (*PatchPt != '\r')
|
|
*str++ = *PatchPt;
|
|
else
|
|
size++;
|
|
|
|
PatchPt++;
|
|
} while (--size && *PatchPt != 0);
|
|
|
|
*str = 0;
|
|
return true;
|
|
}
|
|
|
|
static void ReplaceSpecialChars (char *str)
|
|
{
|
|
char *p = str, c;
|
|
int i;
|
|
|
|
while ( (c = *p++) ) {
|
|
if (c != '\\') {
|
|
*str++ = c;
|
|
} else {
|
|
switch (*p) {
|
|
case 'n':
|
|
case 'N':
|
|
*str++ = '\n';
|
|
break;
|
|
case 't':
|
|
case 'T':
|
|
*str++ = '\t';
|
|
break;
|
|
case 'r':
|
|
case 'R':
|
|
*str++ = '\r';
|
|
break;
|
|
case 'x':
|
|
case 'X':
|
|
c = 0;
|
|
p++;
|
|
for (i = 0; i < 2; i++) {
|
|
c <<= 4;
|
|
if (*p >= '0' && *p <= '9')
|
|
c += *p-'0';
|
|
else if (*p >= 'a' && *p <= 'f')
|
|
c += 10 + *p-'a';
|
|
else if (*p >= 'A' && *p <= 'F')
|
|
c += 10 + *p-'A';
|
|
else
|
|
break;
|
|
p++;
|
|
}
|
|
*str++ = c;
|
|
break;
|
|
case '0':
|
|
case '1':
|
|
case '2':
|
|
case '3':
|
|
case '4':
|
|
case '5':
|
|
case '6':
|
|
case '7':
|
|
c = 0;
|
|
for (i = 0; i < 3; i++) {
|
|
c <<= 3;
|
|
if (*p >= '0' && *p <= '7')
|
|
c += *p-'0';
|
|
else
|
|
break;
|
|
p++;
|
|
}
|
|
*str++ = c;
|
|
break;
|
|
default:
|
|
*str++ = *p;
|
|
break;
|
|
}
|
|
p++;
|
|
}
|
|
}
|
|
*str = 0;
|
|
}
|
|
|
|
static char *skipwhite (char *str)
|
|
{
|
|
if (str)
|
|
while (*str && isspace(*str))
|
|
str++;
|
|
return str;
|
|
}
|
|
|
|
static void stripwhite (char *str)
|
|
{
|
|
char *end = str + strlen(str) - 1;
|
|
|
|
while (end >= str && isspace(*end))
|
|
end--;
|
|
|
|
end[1] = '\0';
|
|
}
|
|
|
|
static char *igets (void)
|
|
{
|
|
char *line;
|
|
|
|
if (*PatchPt == '\0' || PatchPt >= PatchFile + PatchSize )
|
|
return NULL;
|
|
|
|
line = PatchPt;
|
|
|
|
while (*PatchPt != '\n' && *PatchPt != '\0')
|
|
PatchPt++;
|
|
|
|
if (*PatchPt == '\n')
|
|
*PatchPt++ = 0;
|
|
|
|
return line;
|
|
}
|
|
|
|
static int GetLine (void)
|
|
{
|
|
char *line, *line2;
|
|
|
|
do {
|
|
while ( (line = igets ()) )
|
|
if (line[0] != '#') // Skip comment lines
|
|
break;
|
|
|
|
if (!line)
|
|
return 0;
|
|
|
|
Line1 = skipwhite (line);
|
|
} while (Line1 && *Line1 == 0); // Loop until we get a line with
|
|
// more than just whitespace.
|
|
line = strchr (Line1, '=');
|
|
|
|
if (line) { // We have an '=' in the input line
|
|
line2 = line;
|
|
while (--line2 >= Line1)
|
|
if (*line2 > ' ')
|
|
break;
|
|
|
|
if (line2 < Line1)
|
|
return 0; // Nothing before '='
|
|
|
|
*(line2 + 1) = 0;
|
|
|
|
line++;
|
|
while (*line && *line <= ' ')
|
|
line++;
|
|
|
|
if (*line == 0)
|
|
return 0; // Nothing after '='
|
|
|
|
Line2 = line;
|
|
|
|
return 1;
|
|
} else { // No '=' in input line
|
|
line = Line1 + 1;
|
|
while (*line > ' ')
|
|
line++; // Get beyond first word
|
|
|
|
*line++ = 0;
|
|
while (*line && *line <= ' ')
|
|
line++; // Skip white space
|
|
|
|
//.bex files don't have this restriction
|
|
//if (*line == 0)
|
|
// return 0; // No second word
|
|
|
|
Line2 = line;
|
|
|
|
return 2;
|
|
}
|
|
}
|
|
|
|
// misc1 = vrange (arg +3), misc2 = hrange (arg+4)
|
|
static int CreateMushroomFunc(VMFunctionBuilder &buildit, int value1, int value2)
|
|
{ // A_Mushroom
|
|
buildit.Emit(OP_PARAM, 0, REGT_NIL, 0); // spawntype
|
|
buildit.Emit(OP_PARAM, 0, REGT_NIL, 0); // numspawns
|
|
buildit.Emit(OP_PARAMI, 1); // flag
|
|
// vrange
|
|
if (value1 == 0)
|
|
{
|
|
buildit.Emit(OP_PARAM, 0, REGT_NIL, 0);
|
|
}
|
|
else
|
|
{
|
|
buildit.Emit(OP_PARAM, 0, REGT_FLOAT | REGT_KONST, buildit.GetConstantFloat(DEHToDouble(value1)));
|
|
}
|
|
// hrange
|
|
if (value2 == 0)
|
|
{
|
|
buildit.Emit(OP_PARAM, 0, REGT_NIL, 0);
|
|
}
|
|
else
|
|
{
|
|
buildit.Emit(OP_PARAM, 0, REGT_FLOAT | REGT_KONST, buildit.GetConstantFloat(DEHToDouble(value2)));
|
|
}
|
|
return 5;
|
|
}
|
|
|
|
// misc1 = type (arg +0), misc2 = Z-pos (arg +2)
|
|
static int CreateSpawnFunc(VMFunctionBuilder &buildit, int value1, int value2)
|
|
{ // A_SpawnItem
|
|
if (InfoNames[value1-1] == NULL)
|
|
{
|
|
I_Error("No class found for dehackednum %d!\n", value1+1);
|
|
return 0;
|
|
}
|
|
int typereg = buildit.GetConstantAddress(InfoNames[value1-1], ATAG_OBJECT);
|
|
int heightreg = buildit.GetConstantFloat(value2);
|
|
|
|
buildit.Emit(OP_PARAM, 0, REGT_POINTER | REGT_KONST, typereg); // itemtype
|
|
buildit.Emit(OP_PARAM, 0, REGT_NIL, 0); // distance
|
|
buildit.Emit(OP_PARAM, 0, REGT_FLOAT | REGT_KONST, heightreg); // height
|
|
// The rest of the parameters to A_SpawnItem can just keep their defaults
|
|
return 3;
|
|
}
|
|
|
|
// misc1 = angle (in degrees) (arg +0 but factor in current actor angle too)
|
|
static int CreateTurnFunc(VMFunctionBuilder &buildit, int value1, int value2)
|
|
{ // A_Turn
|
|
buildit.Emit(OP_PARAM, 0, REGT_FLOAT | REGT_KONST, buildit.GetConstantFloat(value1)); // angle
|
|
return 1;
|
|
}
|
|
|
|
// misc1 = angle (in degrees) (arg +0)
|
|
static int CreateFaceFunc(VMFunctionBuilder &buildit, int value1, int value2)
|
|
{ // A_FaceTarget
|
|
buildit.Emit(OP_PARAM, 0, REGT_FLOAT | REGT_KONST, buildit.GetConstantFloat(value1)); // angle
|
|
return 1;
|
|
}
|
|
|
|
// misc1 = damage, misc 2 = sound
|
|
static int CreateScratchFunc(VMFunctionBuilder &buildit, int value1, int value2)
|
|
{ // A_CustomMeleeAttack
|
|
buildit.EmitParamInt(value1); // damage
|
|
if (value2)
|
|
{
|
|
buildit.EmitParamInt(SoundMap[value2-1]); // hit sound
|
|
return 2;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
// misc1 = sound, misc2 = attenuation none (true) or normal (false)
|
|
static int CreatePlaySoundFunc(VMFunctionBuilder &buildit, int value1, int value2)
|
|
{ // A_PlaySound
|
|
int float1 = buildit.GetConstantFloat(1);
|
|
int attenreg = buildit.GetConstantFloat(value2 ? ATTN_NONE : ATTN_NORM);
|
|
|
|
buildit.EmitParamInt(SoundMap[value1-1]); // soundid
|
|
buildit.Emit(OP_PARAMI, CHAN_BODY); // channel
|
|
buildit.Emit(OP_PARAM, 0, REGT_FLOAT | REGT_KONST, float1); // volume
|
|
buildit.Emit(OP_PARAMI, false); // looping
|
|
buildit.Emit(OP_PARAM, 0, REGT_FLOAT | REGT_KONST, attenreg); // attenuation
|
|
return 5;
|
|
}
|
|
|
|
// misc1 = state, misc2 = probability
|
|
static int CreateRandomJumpFunc(VMFunctionBuilder &buildit, int value1, int value2)
|
|
{ // A_Jump
|
|
int statereg = buildit.GetConstantAddress(FindState(value1), ATAG_STATE);
|
|
|
|
buildit.EmitParamInt(value2); // maxchance
|
|
buildit.Emit(OP_PARAM, 0, REGT_POINTER | REGT_KONST, statereg); // jumpto
|
|
return 2;
|
|
}
|
|
|
|
// misc1 = Boom linedef type, misc2 = sector tag
|
|
static int CreateLineEffectFunc(VMFunctionBuilder &buildit, int value1, int value2)
|
|
{ // A_LineEffect
|
|
// This is the second MBF codepointer that couldn't be translated easily.
|
|
// Calling P_TranslateLineDef() here was a simple matter, as was adding an
|
|
// extra parameter to A_CallSpecial so as to replicate the LINEDONE stuff,
|
|
// but unfortunately DEHACKED lumps are processed before the map translation
|
|
// arrays are initialized so this didn't work.
|
|
buildit.EmitParamInt(value1); // special
|
|
buildit.EmitParamInt(value2); // tag
|
|
return 2;
|
|
}
|
|
|
|
// No misc, but it's basically A_Explode with an added effect
|
|
static int CreateNailBombFunc(VMFunctionBuilder &buildit, int value1, int value2)
|
|
{ // A_Explode
|
|
// This one does not actually have MBF-style parameters. But since
|
|
// we're aliasing it to an extension of A_Explode...
|
|
buildit.Emit(OP_PARAM, 0, REGT_NIL, 0); // damage
|
|
buildit.Emit(OP_PARAM, 0, REGT_NIL, 0); // distance
|
|
buildit.Emit(OP_PARAM, 0, REGT_NIL, 0); // flags
|
|
buildit.Emit(OP_PARAM, 0, REGT_NIL, 0); // alert
|
|
buildit.Emit(OP_PARAM, 0, REGT_NIL, 0); // fulldamagedistance
|
|
buildit.Emit(OP_PARAMI, 30); // nails
|
|
buildit.Emit(OP_PARAMI, 10); // naildamage
|
|
return 7;
|
|
}
|
|
|
|
// This array must be in sync with the Aliases array in DEHSUPP.
|
|
static int (*MBFCodePointerFactories[])(VMFunctionBuilder&, int, int) =
|
|
{
|
|
// Die and Detonate are not in this list because these codepointers have
|
|
// no dehacked arguments and therefore do not need special handling.
|
|
// NailBomb has no argument but is implemented as new parameters for A_Explode.
|
|
CreateMushroomFunc,
|
|
CreateSpawnFunc,
|
|
CreateTurnFunc,
|
|
CreateFaceFunc,
|
|
CreateScratchFunc,
|
|
CreatePlaySoundFunc,
|
|
CreateRandomJumpFunc,
|
|
CreateLineEffectFunc,
|
|
CreateNailBombFunc
|
|
};
|
|
|
|
// Creates new functions for the given state so as to convert MBF-args (misc1 and misc2) into real args.
|
|
|
|
void SetDehParams(FState *state, int codepointer)
|
|
{
|
|
int value1 = state->GetMisc1();
|
|
int value2 = state->GetMisc2();
|
|
if (!(value1|value2)) return;
|
|
|
|
// Fakey fake script position thingamajig. Because NULL cannot be used instead.
|
|
// Even if the lump was parsed by an FScanner, there would hardly be a way to
|
|
// identify which line is troublesome.
|
|
FScriptPosition *pos = new FScriptPosition(FString("DEHACKED"), 0);
|
|
|
|
// Let's identify the codepointer we're dealing with.
|
|
PFunction *sym;
|
|
sym = dyn_cast<PFunction>(RUNTIME_CLASS(AInventory)->Symbols.FindSymbol(FName(MBFCodePointers[codepointer].name), true));
|
|
if (sym == NULL) return;
|
|
|
|
if (codepointer < 0 || (unsigned)codepointer >= countof(MBFCodePointerFactories))
|
|
{
|
|
// This simply should not happen.
|
|
Printf("Unmanaged dehacked codepointer alias num %i\n", codepointer);
|
|
}
|
|
else
|
|
{
|
|
VMFunctionBuilder buildit;
|
|
// Allocate registers used to pass parameters in.
|
|
// self, stateowner, state (all are pointers)
|
|
buildit.Registers[REGT_POINTER].Get(NAP);
|
|
// Emit code to pass the standard action function parameters.
|
|
for (int i = 0; i < NAP; i++)
|
|
{
|
|
buildit.Emit(OP_PARAM, 0, REGT_POINTER, i);
|
|
}
|
|
// Emit code for action parameters.
|
|
int argcount = MBFCodePointerFactories[codepointer](buildit, value1, value2);
|
|
buildit.Emit(OP_TAIL_K, buildit.GetConstantAddress(sym->Variants[0].Implementation, ATAG_OBJECT), NAP + argcount, 0);
|
|
// Attach it to the state.
|
|
VMScriptFunction *sfunc = buildit.MakeFunction();
|
|
sfunc->NumArgs = NAP;
|
|
state->SetAction(sfunc);
|
|
}
|
|
}
|
|
|
|
static int PatchThing (int thingy)
|
|
{
|
|
enum
|
|
{
|
|
// Boom flags
|
|
MF_TRANSLATION = 0x0c000000, // if 0x4 0x8 or 0xc, use a translation
|
|
MF_TRANSSHIFT = 26, // table for player colormaps
|
|
// A couple of Boom flags that don't exist in ZDoom
|
|
MF_SLIDE = 0x00002000, // Player: keep info about sliding along walls.
|
|
MF_TRANSLUCENT = 0x80000000, // Translucent sprite?
|
|
// MBF flags: TOUCHY is remapped to flags6, FRIEND is turned into FRIENDLY,
|
|
// and finally BOUNCES is replaced by bouncetypes with the BOUNCES_MBF bit.
|
|
MF_TOUCHY = 0x10000000, // killough 11/98: dies when solids touch it
|
|
MF_BOUNCES = 0x20000000, // killough 7/11/98: for beta BFG fireballs
|
|
MF_FRIEND = 0x40000000, // killough 7/18/98: friendly monsters
|
|
};
|
|
|
|
int result;
|
|
AActor *info;
|
|
BYTE dummy[sizeof(AActor)];
|
|
bool hadHeight = false;
|
|
bool hadTranslucency = false;
|
|
bool hadStyle = false;
|
|
FStateDefinitions statedef;
|
|
bool patchedStates = false;
|
|
ActorFlags oldflags;
|
|
PClassActor *type;
|
|
SWORD *ednum, dummyed;
|
|
|
|
type = NULL;
|
|
info = (AActor *)&dummy;
|
|
ednum = &dummyed;
|
|
if (thingy > (int)InfoNames.Size() || thingy <= 0)
|
|
{
|
|
Printf ("Thing %d out of range.\n", thingy);
|
|
}
|
|
else
|
|
{
|
|
DPrintf ("Thing %d\n", thingy);
|
|
if (thingy > 0)
|
|
{
|
|
type = InfoNames[thingy - 1];
|
|
if (type == NULL)
|
|
{
|
|
info = (AActor *)&dummy;
|
|
ednum = &dummyed;
|
|
// An error for the name has already been printed while loading DEHSUPP.
|
|
Printf ("Could not find thing %d\n", thingy);
|
|
}
|
|
else
|
|
{
|
|
info = GetDefaultByType (type);
|
|
ednum = &type->DoomEdNum;
|
|
}
|
|
}
|
|
}
|
|
|
|
oldflags = info->flags;
|
|
|
|
while ((result = GetLine ()) == 1)
|
|
{
|
|
char *endptr;
|
|
unsigned long val = strtoul (Line2, &endptr, 10);
|
|
size_t linelen = strlen (Line1);
|
|
|
|
if (linelen == 10 && stricmp (Line1, "Hit points") == 0)
|
|
{
|
|
info->health = val;
|
|
}
|
|
else if (linelen == 13 && stricmp (Line1, "Reaction time") == 0)
|
|
{
|
|
info->reactiontime = val;
|
|
}
|
|
else if (linelen == 11 && stricmp (Line1, "Pain chance") == 0)
|
|
{
|
|
info->PainChance = (SWORD)val;
|
|
}
|
|
else if (linelen == 12 && stricmp (Line1, "Translucency") == 0)
|
|
{
|
|
info->Alpha = DEHToDouble(val);
|
|
info->RenderStyle = STYLE_Translucent;
|
|
hadTranslucency = true;
|
|
hadStyle = true;
|
|
}
|
|
else if (linelen == 6 && stricmp (Line1, "Height") == 0)
|
|
{
|
|
info->Height = DEHToDouble(val);
|
|
info->projectilepassheight = 0; // needs to be disabled
|
|
hadHeight = true;
|
|
}
|
|
else if (linelen == 14 && stricmp (Line1, "Missile damage") == 0)
|
|
{
|
|
info->Damage = CreateDamageFunction(val);
|
|
}
|
|
else if (linelen == 5)
|
|
{
|
|
if (stricmp (Line1, "Speed") == 0)
|
|
{
|
|
info->Speed = (signed long)val; // handle fixed point later.
|
|
}
|
|
else if (stricmp (Line1, "Width") == 0)
|
|
{
|
|
info->radius = DEHToDouble(val);
|
|
}
|
|
else if (stricmp (Line1, "Alpha") == 0)
|
|
{
|
|
info->Alpha = atof (Line2);
|
|
hadTranslucency = true;
|
|
}
|
|
else if (stricmp (Line1, "Scale") == 0)
|
|
{
|
|
info->Scale.Y = info->Scale.X = clamp(atof (Line2), 1./65536, 256.);
|
|
}
|
|
else if (stricmp (Line1, "Decal") == 0)
|
|
{
|
|
stripwhite (Line2);
|
|
const FDecalTemplate *decal = DecalLibrary.GetDecalByName (Line2);
|
|
if (decal != NULL)
|
|
{
|
|
info->DecalGenerator = const_cast <FDecalTemplate *>(decal);
|
|
}
|
|
else
|
|
{
|
|
Printf ("Thing %d: Unknown decal %s\n", thingy, Line2);
|
|
}
|
|
}
|
|
}
|
|
else if (linelen == 12 && stricmp (Line1, "Render Style") == 0)
|
|
{
|
|
stripwhite (Line2);
|
|
int style = FindStyle (Line2);
|
|
if (style >= 0)
|
|
{
|
|
info->RenderStyle = ERenderStyle(style);
|
|
hadStyle = true;
|
|
}
|
|
}
|
|
else if (linelen > 6)
|
|
{
|
|
if (linelen == 12 && stricmp (Line1, "No Ice Death") == 0)
|
|
{
|
|
if (val)
|
|
{
|
|
info->flags4 |= MF4_NOICEDEATH;
|
|
}
|
|
else
|
|
{
|
|
info->flags4 &= ~MF4_NOICEDEATH;
|
|
}
|
|
}
|
|
else if (stricmp (Line1 + linelen - 6, " frame") == 0)
|
|
{
|
|
FState *state = FindState (val);
|
|
|
|
if (type != NULL && !patchedStates)
|
|
{
|
|
statedef.MakeStateDefines(type);
|
|
patchedStates = true;
|
|
}
|
|
|
|
if (!strnicmp (Line1, "Initial", 7))
|
|
statedef.SetStateLabel("Spawn", state ? state : GetDefault<AActor>()->SpawnState);
|
|
else if (!strnicmp (Line1, "First moving", 12))
|
|
statedef.SetStateLabel("See", state);
|
|
else if (!strnicmp (Line1, "Injury", 6))
|
|
statedef.SetStateLabel("Pain", state);
|
|
else if (!strnicmp (Line1, "Close attack", 12))
|
|
{
|
|
if (thingy != 1) // Not for players!
|
|
{
|
|
statedef.SetStateLabel("Melee", state);
|
|
}
|
|
}
|
|
else if (!strnicmp (Line1, "Far attack", 10))
|
|
{
|
|
if (thingy != 1) // Not for players!
|
|
{
|
|
statedef.SetStateLabel("Missile", state);
|
|
}
|
|
}
|
|
else if (!strnicmp (Line1, "Death", 5))
|
|
statedef.SetStateLabel("Death", state);
|
|
else if (!strnicmp (Line1, "Exploding", 9))
|
|
statedef.SetStateLabel("XDeath", state);
|
|
else if (!strnicmp (Line1, "Respawn", 7))
|
|
statedef.SetStateLabel("Raise", state);
|
|
}
|
|
else if (stricmp (Line1 + linelen - 6, " sound") == 0)
|
|
{
|
|
FSoundID snd;
|
|
|
|
if (val == 0 || val >= SoundMap.Size())
|
|
{
|
|
if (endptr == Line2)
|
|
{ // Sound was not a (valid) number,
|
|
// so treat it as an actual sound name.
|
|
stripwhite (Line2);
|
|
snd = Line2;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
snd = SoundMap[val-1];
|
|
}
|
|
|
|
if (!strnicmp (Line1, "Alert", 5))
|
|
info->SeeSound = snd;
|
|
else if (!strnicmp (Line1, "Attack", 6))
|
|
info->AttackSound = snd;
|
|
else if (!strnicmp (Line1, "Pain", 4))
|
|
info->PainSound = snd;
|
|
else if (!strnicmp (Line1, "Death", 5))
|
|
info->DeathSound = snd;
|
|
else if (!strnicmp (Line1, "Action", 6))
|
|
info->ActiveSound = snd;
|
|
}
|
|
}
|
|
else if (linelen == 4)
|
|
{
|
|
if (stricmp (Line1, "Mass") == 0)
|
|
{
|
|
info->Mass = val;
|
|
}
|
|
else if (stricmp (Line1, "Bits") == 0)
|
|
{
|
|
DWORD value[4] = { 0, 0, 0 };
|
|
bool vchanged[4] = { false, false, false };
|
|
// ZDoom used to block the upper range of bits to force use of mnemonics for extra flags.
|
|
// MBF also defined extra flags in the same range, but without forcing mnemonics. For MBF
|
|
// compatibility, the upper bits are freed, but we have conflicts between the ZDoom bits
|
|
// and the MBF bits. The only such flag exposed to DEHSUPP, though, is STEALTH -- the others
|
|
// are not available through mnemonics, and aren't available either through their bit value.
|
|
// So if we find the STEALTH keyword, it's a ZDoom mod, otherwise assume FRIEND.
|
|
bool zdoomflags = false;
|
|
char *strval;
|
|
|
|
for (strval = Line2; (strval = strtok (strval, ",+| \t\f\r")); strval = NULL)
|
|
{
|
|
if (IsNum (strval))
|
|
{
|
|
// I have no idea why everyone insists on using strtol here even though it fails
|
|
// dismally if a value is parsed where the highest bit it set. Do people really
|
|
// use negative values here? Let's better be safe and check both.
|
|
if (strchr(strval, '-')) value[0] |= (unsigned long)strtol(strval, NULL, 10);
|
|
else value[0] |= (unsigned long)strtoul(strval, NULL, 10);
|
|
vchanged[0] = true;
|
|
}
|
|
else
|
|
{
|
|
// STEALTH FRIEND HACK!
|
|
if (!stricmp(strval, "STEALTH")) zdoomflags = true;
|
|
unsigned i;
|
|
for(i = 0; i < BitNames.Size(); i++)
|
|
{
|
|
if (!stricmp(strval, BitNames[i].Name))
|
|
{
|
|
vchanged[BitNames[i].WhichFlags] = true;
|
|
value[BitNames[i].WhichFlags] |= 1 << BitNames[i].Bit;
|
|
break;
|
|
}
|
|
}
|
|
if (i == BitNames.Size())
|
|
{
|
|
DPrintf("Unknown bit mnemonic %s\n", strval);
|
|
}
|
|
}
|
|
}
|
|
if (vchanged[0])
|
|
{
|
|
if (value[0] & MF_SLIDE)
|
|
{
|
|
// SLIDE (which occupies in Doom what is the MF_INCHASE slot in ZDoom)
|
|
value[0] &= ~MF_SLIDE; // clean the slot
|
|
// Nothing else to do, this flag is never actually used.
|
|
}
|
|
if (value[0] & MF_TRANSLATION)
|
|
{
|
|
info->Translation = TRANSLATION (TRANSLATION_Standard,
|
|
((value[0] & MF_TRANSLATION) >> (MF_TRANSSHIFT))-1);
|
|
value[0] &= ~MF_TRANSLATION;
|
|
}
|
|
if (value[0] & MF_TOUCHY)
|
|
{
|
|
// TOUCHY (which occupies in MBF what is the MF_UNMORPHED slot in ZDoom)
|
|
value[0] &= ~MF_TOUCHY; // clean the slot
|
|
info->flags6 |= MF6_TOUCHY; // remap the flag
|
|
}
|
|
if (value[0] & MF_BOUNCES)
|
|
{
|
|
// BOUNCES (which occupies in MBF the MF_NOLIFTDROP slot)
|
|
// This flag is especially convoluted as what it does depend on what
|
|
// other flags the actor also has, and whether it's "sentient" or not.
|
|
value[0] &= ~MF_BOUNCES; // clean the slot
|
|
|
|
// MBF considers that things that bounce can be damaged, even if not shootable.
|
|
info->flags6 |= MF6_VULNERABLE;
|
|
// MBF also considers that bouncers pass through blocking lines as projectiles.
|
|
info->flags3 |= MF3_NOBLOCKMONST;
|
|
// MBF also considers that bouncers that explode are grenades, and MBF grenades
|
|
// are supposed to hurt everything, except cyberdemons if they're fired by cybies.
|
|
// Let's translate that in a more generic way as grenades which hurt everything
|
|
// except the class of their shooter. Yes, it does diverge a bit from MBF, as for
|
|
// example a dehacked arachnotron that shoots grenade would kill itself quickly
|
|
// in MBF and will not here. But class-specific checks are cumbersome and limiting.
|
|
info->flags4 |= (MF4_FORCERADIUSDMG | MF4_DONTHARMCLASS);
|
|
|
|
// MBF bouncing missiles rebound on floors and ceiling, but not on walls.
|
|
// This is different from BOUNCE_Heretic behavior as in Heretic the missiles
|
|
// die when they bounce, while in MBF they will continue to bounce until they
|
|
// collide with a wall or a solid actor.
|
|
if (value[0] & MF_MISSILE) info->BounceFlags = BOUNCE_Classic;
|
|
// MBF bouncing actors that do not have the missile flag will also rebound on
|
|
// walls, and this does correspond roughly to the ZDoom bounce style.
|
|
else info->BounceFlags = BOUNCE_Grenade;
|
|
|
|
// MBF grenades are dehacked rockets that gain the BOUNCES flag but
|
|
// lose the MISSILE flag, so they can be identified here easily.
|
|
if (!(value[0] & MF_MISSILE) && info->effects & FX_ROCKET)
|
|
{
|
|
info->effects &= ~FX_ROCKET; // replace rocket trail
|
|
info->effects |= FX_GRENADE; // by grenade trail
|
|
}
|
|
|
|
// MBF bounce factors depend on flag combos:
|
|
const double MBF_BOUNCE_NOGRAVITY = 1; // With NOGRAVITY: full momentum
|
|
const double MBF_BOUNCE_FLOATDROPOFF = 0.85; // With FLOAT and DROPOFF: 85%
|
|
const double MBF_BOUNCE_FLOAT = 0.7; // With FLOAT alone: 70%
|
|
const double MBF_BOUNCE_DEFAULT = 0.45; // Without the above flags: 45%
|
|
const double MBF_BOUNCE_WALL = 0.5; // Bouncing off walls: 50%
|
|
|
|
info->bouncefactor = ((value[0] & MF_NOGRAVITY) ? MBF_BOUNCE_NOGRAVITY
|
|
: (value[0] & MF_FLOAT) ? (value[0] & MF_DROPOFF) ? MBF_BOUNCE_FLOATDROPOFF
|
|
: MBF_BOUNCE_FLOAT : MBF_BOUNCE_DEFAULT);
|
|
|
|
info->wallbouncefactor = ((value[0] & MF_NOGRAVITY) ? MBF_BOUNCE_NOGRAVITY : MBF_BOUNCE_WALL);
|
|
|
|
// MBF sentient actors with BOUNCE and FLOAT are able to "jump" by floating up.
|
|
if (info->IsSentient())
|
|
{
|
|
if (value[0] & MF_FLOAT) info->flags6 |= MF6_CANJUMP;
|
|
}
|
|
// Non sentient actors can be damaged but they shouldn't bleed.
|
|
else
|
|
{
|
|
value[0] |= MF_NOBLOOD;
|
|
}
|
|
}
|
|
if (zdoomflags && (value [0] & MF_STEALTH))
|
|
{
|
|
// STEALTH FRIEND HACK!
|
|
}
|
|
else if (value[0] & MF_FRIEND)
|
|
{
|
|
// FRIEND (which occupies in MBF the MF_STEALTH slot)
|
|
value[0] &= ~MF_FRIEND; // clean the slot
|
|
value[0] |= MF_FRIENDLY; // remap the flag to its ZDoom equivalent
|
|
// MBF friends are not blocked by monster blocking lines:
|
|
info->flags3 |= MF3_NOBLOCKMONST;
|
|
}
|
|
if (value[0] & MF_TRANSLUCENT)
|
|
{
|
|
// TRANSLUCENT (which occupies in Boom the MF_ICECORPSE slot)
|
|
value[0] &= ~MF_TRANSLUCENT; // clean the slot
|
|
vchanged[2] = true; value[2] |= 2; // let the TRANSLUCxx code below handle it
|
|
}
|
|
if ((info->flags & MF_MISSILE) && (info->flags2 & MF2_NOTELEPORT)
|
|
&& !(value[0] & MF_MISSILE))
|
|
{
|
|
// ZDoom gives missiles flags that did not exist in Doom: MF2_NOTELEPORT,
|
|
// MF2_IMPACT, and MF2_PCROSS. The NOTELEPORT one can be a problem since
|
|
// some projectile actors (those new to Doom II) were not excluded from
|
|
// triggering line effects and can teleport when the missile flag is removed.
|
|
info->flags2 &= ~MF2_NOTELEPORT;
|
|
}
|
|
info->flags = ActorFlags::FromInt (value[0]);
|
|
}
|
|
if (vchanged[1])
|
|
{
|
|
if (value[1] & 0x00000004) // old BOUNCE1
|
|
{
|
|
value[1] &= ~0x00000004;
|
|
info->BounceFlags = BOUNCE_DoomCompat;
|
|
}
|
|
// Damage types that once were flags but now are not
|
|
if (value[1] & 0x20000000)
|
|
{
|
|
info->DamageType = NAME_Ice;
|
|
value[1] &= ~0x20000000;
|
|
}
|
|
if (value[1] & 0x10000000)
|
|
{
|
|
info->DamageType = NAME_Fire;
|
|
value[1] &= ~0x10000000;
|
|
}
|
|
if (value[1] & 0x00000001)
|
|
{
|
|
info->Gravity = 1./4;
|
|
value[1] &= ~0x00000001;
|
|
}
|
|
info->flags2 = ActorFlags2::FromInt (value[1]);
|
|
}
|
|
if (vchanged[2])
|
|
{
|
|
if (value[2] & 7)
|
|
{
|
|
hadTranslucency = true;
|
|
if (value[2] & 1)
|
|
info->Alpha = 0.25;
|
|
else if (value[2] & 2)
|
|
info->Alpha = 0.5;
|
|
else if (value[2] & 4)
|
|
info->Alpha = 0.75;
|
|
info->RenderStyle = STYLE_Translucent;
|
|
}
|
|
if (value[2] & 8)
|
|
info->renderflags |= RF_INVISIBLE;
|
|
else
|
|
info->renderflags &= ~RF_INVISIBLE;
|
|
}
|
|
DPrintf ("Bits: %d,%d (0x%08x,0x%08x)\n", info->flags.GetValue(), info->flags2.GetValue(),
|
|
info->flags.GetValue(), info->flags2.GetValue());
|
|
}
|
|
else if (stricmp (Line1, "ID #") == 0)
|
|
{
|
|
*ednum = (SWORD)val;
|
|
}
|
|
}
|
|
else Printf (unknown_str, Line1, "Thing", thingy);
|
|
}
|
|
|
|
if (info != (AActor *)&dummy)
|
|
{
|
|
// Reset heights for things hanging from the ceiling that
|
|
// don't specify a new height.
|
|
if (info->flags & MF_SPAWNCEILING &&
|
|
!hadHeight &&
|
|
thingy <= (int)OrgHeights.Size() && thingy > 0)
|
|
{
|
|
info->Height = OrgHeights[thingy - 1];
|
|
info->projectilepassheight = 0;
|
|
}
|
|
// If the thing's shadow changed, change its fuzziness if not already specified
|
|
if ((info->flags ^ oldflags) & MF_SHADOW)
|
|
{
|
|
if (info->flags & MF_SHADOW)
|
|
{ // changed to shadow
|
|
if (!hadStyle)
|
|
info->RenderStyle = STYLE_OptFuzzy;
|
|
if (!hadTranslucency)
|
|
info->Alpha = 0.5;
|
|
}
|
|
else
|
|
{ // changed from shadow
|
|
if (!hadStyle)
|
|
info->RenderStyle = STYLE_Normal;
|
|
}
|
|
}
|
|
// Speed could be either an int of fixed value, depending on its use
|
|
// If this value is very large it needs to be rescaled.
|
|
if (fabs(info->Speed) >= 256)
|
|
{
|
|
info->Speed /= 65536;
|
|
}
|
|
|
|
if (info->flags & MF_SPECIAL)
|
|
{
|
|
PushTouchedActor(const_cast<PClassActor *>(type));
|
|
}
|
|
|
|
// If MF_COUNTKILL is set, make sure the other standard monster flags are
|
|
// set, too. And vice versa.
|
|
if (thingy != 1) // don't mess with the player's flags
|
|
{
|
|
if (info->flags & MF_COUNTKILL)
|
|
{
|
|
info->flags2 |= MF2_PUSHWALL | MF2_MCROSS | MF2_PASSMOBJ;
|
|
info->flags3 |= MF3_ISMONSTER;
|
|
}
|
|
else
|
|
{
|
|
info->flags2 &= ~(MF2_PUSHWALL | MF2_MCROSS);
|
|
info->flags3 &= ~MF3_ISMONSTER;
|
|
}
|
|
}
|
|
// Everything that's altered here gets the CANUSEWALLS flag, just in case
|
|
// it calls P_Move().
|
|
info->flags4 |= MF4_CANUSEWALLS;
|
|
if (patchedStates)
|
|
{
|
|
statedef.InstallStates(type, info);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
// The only remotely useful thing Dehacked sound patches could do
|
|
// was change where the sound's name was stored. Since there is no
|
|
// real benefit to doing this, and it would be very difficult for
|
|
// me to emulate it, I have disabled them entirely.
|
|
|
|
static int PatchSound (int soundNum)
|
|
{
|
|
int result;
|
|
|
|
DPrintf ("Sound %d (no longer supported)\n", soundNum);
|
|
/*
|
|
sfxinfo_t *info, dummy;
|
|
int offset = 0;
|
|
if (soundNum >= 1 && soundNum <= NUMSFX) {
|
|
info = &S_sfx[soundNum];
|
|
} else {
|
|
info = &dummy;
|
|
Printf ("Sound %d out of range.\n");
|
|
}
|
|
*/
|
|
while ((result = GetLine ()) == 1) {
|
|
/*
|
|
if (!stricmp ("Offset", Line1))
|
|
offset = atoi (Line2);
|
|
else CHECKKEY ("Zero/One", info->singularity)
|
|
else CHECKKEY ("Value", info->priority)
|
|
else CHECKKEY ("Zero 1", info->link)
|
|
else CHECKKEY ("Neg. One 1", info->pitch)
|
|
else CHECKKEY ("Neg. One 2", info->volume)
|
|
else CHECKKEY ("Zero 2", info->data)
|
|
else CHECKKEY ("Zero 3", info->usefulness)
|
|
else CHECKKEY ("Zero 4", info->lumpnum)
|
|
else Printf (unknown_str, Line1, "Sound", soundNum);
|
|
*/
|
|
}
|
|
/*
|
|
if (offset) {
|
|
// Calculate offset from start of sound names
|
|
offset -= toff[dversion] + 21076;
|
|
|
|
if (offset <= 64) // pistol .. bfg
|
|
offset >>= 3;
|
|
else if (offset <= 260) // sawup .. oof
|
|
offset = (offset + 4) >> 3;
|
|
else // telept .. skeatk
|
|
offset = (offset + 8) >> 3;
|
|
|
|
if (offset >= 0 && offset < NUMSFX) {
|
|
S_sfx[soundNum].name = OrgSfxNames[offset + 1];
|
|
} else {
|
|
Printf ("Sound name %d out of range.\n", offset + 1);
|
|
}
|
|
}
|
|
*/
|
|
return result;
|
|
}
|
|
|
|
static int PatchFrame (int frameNum)
|
|
{
|
|
int result;
|
|
int tics, misc1, frame;
|
|
FState *info, dummy;
|
|
|
|
info = FindState (frameNum);
|
|
if (info)
|
|
{
|
|
DPrintf ("Frame %d\n", frameNum);
|
|
if (frameNum == 47)
|
|
{ // Use original tics for S_DSGUNFLASH1
|
|
tics = 5;
|
|
}
|
|
else if (frameNum == 48)
|
|
{ // Ditto for S_DSGUNFLASH2
|
|
tics = 4;
|
|
}
|
|
else
|
|
{
|
|
tics = info->GetTics ();
|
|
}
|
|
misc1 = info->GetMisc1 ();
|
|
frame = info->GetFrame () | (info->GetFullbright() ? 0x8000 : 0);
|
|
}
|
|
else
|
|
{
|
|
info = &dummy;
|
|
tics = misc1 = frame = 0;
|
|
Printf ("Frame %d out of range\n", frameNum);
|
|
}
|
|
|
|
while ((result = GetLine ()) == 1)
|
|
{
|
|
int val = atoi (Line2);
|
|
size_t keylen = strlen (Line1);
|
|
|
|
if (keylen == 8 && stricmp (Line1, "Duration") == 0)
|
|
{
|
|
tics = clamp (val, -1, SHRT_MAX);
|
|
}
|
|
else if (keylen == 9 && stricmp (Line1, "Unknown 1") == 0)
|
|
{
|
|
misc1 = val;
|
|
}
|
|
else if (keylen == 9 && stricmp (Line1, "Unknown 2") == 0)
|
|
{
|
|
info->Misc2 = val;
|
|
}
|
|
else if (keylen == 13 && stricmp (Line1, "Sprite number") == 0)
|
|
{
|
|
unsigned int i;
|
|
|
|
if (val < (int)OrgSprNames.Size())
|
|
{
|
|
for (i = 0; i < sprites.Size(); i++)
|
|
{
|
|
if (memcmp (OrgSprNames[val].c, sprites[i].name, 4) == 0)
|
|
{
|
|
info->sprite = (int)i;
|
|
break;
|
|
}
|
|
}
|
|
if (i == sprites.Size ())
|
|
{
|
|
Printf ("Frame %d: Sprite %d (%s) is undefined\n",
|
|
frameNum, val, OrgSprNames[val].c);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Printf ("Frame %d: Sprite %d out of range\n", frameNum, val);
|
|
}
|
|
}
|
|
else if (keylen == 10 && stricmp (Line1, "Next frame") == 0)
|
|
{
|
|
info->NextState = FindState (val);
|
|
}
|
|
else if (keylen == 16 && stricmp (Line1, "Sprite subnumber") == 0)
|
|
{
|
|
frame = val;
|
|
}
|
|
else
|
|
{
|
|
Printf (unknown_str, Line1, "Frame", frameNum);
|
|
}
|
|
}
|
|
|
|
if (info != &dummy)
|
|
{
|
|
info->DefineFlags |= SDF_DEHACKED; // Signals the state has been modified by dehacked
|
|
if ((unsigned)(frame & 0x7fff) > 63)
|
|
{
|
|
Printf ("Frame %d: Subnumber must be in range [0,63]\n", frameNum);
|
|
}
|
|
info->Tics = tics;
|
|
info->Misc1 = misc1;
|
|
info->Frame = frame & 0x3f;
|
|
info->Fullbright = frame & 0x8000 ? true : false;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
static int PatchSprite (int sprNum)
|
|
{
|
|
int result;
|
|
int offset = 0;
|
|
|
|
if ((unsigned)sprNum < OrgSprNames.Size())
|
|
{
|
|
DPrintf ("Sprite %d\n", sprNum);
|
|
}
|
|
else
|
|
{
|
|
Printf ("Sprite %d out of range.\n", sprNum);
|
|
sprNum = -1;
|
|
}
|
|
|
|
while ((result = GetLine ()) == 1)
|
|
{
|
|
if (!stricmp ("Offset", Line1))
|
|
offset = atoi (Line2);
|
|
else Printf (unknown_str, Line1, "Sprite", sprNum);
|
|
}
|
|
|
|
if (offset > 0 && sprNum != -1)
|
|
{
|
|
// Calculate offset from beginning of sprite names.
|
|
offset = (offset - toff[dversion] - 22044) / 8;
|
|
|
|
if ((unsigned)offset < OrgSprNames.Size())
|
|
{
|
|
sprNum = FindSprite (OrgSprNames[sprNum].c);
|
|
if (sprNum != -1)
|
|
strncpy (sprites[sprNum].name, OrgSprNames[offset].c, 4);
|
|
}
|
|
else
|
|
{
|
|
Printf ("Sprite name %d out of range.\n", offset);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
static int PatchAmmo (int ammoNum)
|
|
{
|
|
PClassAmmo *ammoType = NULL;
|
|
AAmmo *defaultAmmo = NULL;
|
|
int result;
|
|
int oldclip;
|
|
int dummy;
|
|
int *max = &dummy;
|
|
int *per = &dummy;
|
|
|
|
if (ammoNum >= 0 && ammoNum < 4 && (unsigned)ammoNum <= AmmoNames.Size())
|
|
{
|
|
DPrintf ("Ammo %d.\n", ammoNum);
|
|
ammoType = AmmoNames[ammoNum];
|
|
if (ammoType != NULL)
|
|
{
|
|
defaultAmmo = (AAmmo *)GetDefaultByType (ammoType);
|
|
if (defaultAmmo != NULL)
|
|
{
|
|
max = &defaultAmmo->MaxAmount;
|
|
per = &defaultAmmo->Amount;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (ammoType == NULL)
|
|
{
|
|
Printf ("Ammo %d out of range.\n", ammoNum);
|
|
}
|
|
|
|
oldclip = *per;
|
|
|
|
while ((result = GetLine ()) == 1)
|
|
{
|
|
CHECKKEY ("Max ammo", *max)
|
|
else CHECKKEY ("Per ammo", *per)
|
|
else Printf (unknown_str, Line1, "Ammo", ammoNum);
|
|
}
|
|
|
|
// Calculate the new backpack-given amounts for this ammo.
|
|
if (ammoType != NULL)
|
|
{
|
|
defaultAmmo->BackpackMaxAmount = defaultAmmo->MaxAmount * 2;
|
|
defaultAmmo->BackpackAmount = defaultAmmo->Amount;
|
|
}
|
|
|
|
// Fix per-ammo/max-ammo amounts for descendants of the base ammo class
|
|
if (oldclip != *per)
|
|
{
|
|
for (unsigned int i = 0; i < PClassActor::AllActorClasses.Size(); ++i)
|
|
{
|
|
PClassActor *type = PClassActor::AllActorClasses[i];
|
|
|
|
if (type == ammoType)
|
|
continue;
|
|
|
|
if (type->IsDescendantOf (ammoType))
|
|
{
|
|
defaultAmmo = (AAmmo *)GetDefaultByType (type);
|
|
defaultAmmo->MaxAmount = *max;
|
|
defaultAmmo->Amount = Scale (defaultAmmo->Amount, *per, oldclip);
|
|
}
|
|
else if (type->IsDescendantOf (RUNTIME_CLASS(AWeapon)))
|
|
{
|
|
AWeapon *defWeap = (AWeapon *)GetDefaultByType (type);
|
|
if (defWeap->AmmoType1 == ammoType)
|
|
{
|
|
defWeap->AmmoGive1 = Scale (defWeap->AmmoGive1, *per, oldclip);
|
|
}
|
|
if (defWeap->AmmoType2 == ammoType)
|
|
{
|
|
defWeap->AmmoGive2 = Scale (defWeap->AmmoGive2, *per, oldclip);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
static int PatchWeapon (int weapNum)
|
|
{
|
|
int result;
|
|
PClassActor *type = NULL;
|
|
BYTE dummy[sizeof(AWeapon)];
|
|
AWeapon *info = (AWeapon *)&dummy;
|
|
bool patchedStates = false;
|
|
FStateDefinitions statedef;
|
|
|
|
if (weapNum >= 0 && weapNum < 9 && (unsigned)weapNum < WeaponNames.Size())
|
|
{
|
|
type = WeaponNames[weapNum];
|
|
if (type != NULL)
|
|
{
|
|
info = (AWeapon *)GetDefaultByType (type);
|
|
DPrintf ("Weapon %d\n", weapNum);
|
|
}
|
|
}
|
|
|
|
if (type == NULL)
|
|
{
|
|
Printf ("Weapon %d out of range.\n", weapNum);
|
|
}
|
|
|
|
while ((result = GetLine ()) == 1)
|
|
{
|
|
int val = atoi (Line2);
|
|
|
|
if (strlen (Line1) >= 9)
|
|
{
|
|
if (stricmp (Line1 + strlen (Line1) - 6, " frame") == 0)
|
|
{
|
|
FState *state = FindState (val);
|
|
|
|
if (type != NULL && !patchedStates)
|
|
{
|
|
statedef.MakeStateDefines(type);
|
|
patchedStates = true;
|
|
}
|
|
|
|
if (strnicmp (Line1, "Deselect", 8) == 0)
|
|
statedef.SetStateLabel("Select", state);
|
|
else if (strnicmp (Line1, "Select", 6) == 0)
|
|
statedef.SetStateLabel("Deselect", state);
|
|
else if (strnicmp (Line1, "Bobbing", 7) == 0)
|
|
statedef.SetStateLabel("Ready", state);
|
|
else if (strnicmp (Line1, "Shooting", 8) == 0)
|
|
statedef.SetStateLabel("Fire", state);
|
|
else if (strnicmp (Line1, "Firing", 6) == 0)
|
|
statedef.SetStateLabel("Flash", state);
|
|
}
|
|
else if (stricmp (Line1, "Ammo type") == 0)
|
|
{
|
|
if (val < 0 || val >= 12 || (unsigned)val >= AmmoNames.Size())
|
|
{
|
|
val = 5;
|
|
}
|
|
info->AmmoType1 = AmmoNames[val];
|
|
if (info->AmmoType1 != NULL)
|
|
{
|
|
info->AmmoGive1 = ((AAmmo*)GetDefaultByType (info->AmmoType1))->Amount * 2;
|
|
if (info->AmmoUse1 == 0)
|
|
{
|
|
info->AmmoUse1 = 1;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Printf (unknown_str, Line1, "Weapon", weapNum);
|
|
}
|
|
}
|
|
else if (stricmp (Line1, "Decal") == 0)
|
|
{
|
|
stripwhite (Line2);
|
|
const FDecalTemplate *decal = DecalLibrary.GetDecalByName (Line2);
|
|
if (decal != NULL)
|
|
{
|
|
info->DecalGenerator = const_cast <FDecalTemplate *>(decal);
|
|
}
|
|
else
|
|
{
|
|
Printf ("Weapon %d: Unknown decal %s\n", weapNum, Line2);
|
|
}
|
|
}
|
|
else if (stricmp (Line1, "Ammo use") == 0 || stricmp (Line1, "Ammo per shot") == 0)
|
|
{
|
|
info->AmmoUse1 = val;
|
|
info->flags6 |= MF6_INTRYMOVE; // flag the weapon for postprocessing (reuse a flag that can't be set by external means)
|
|
}
|
|
else if (stricmp (Line1, "Min ammo") == 0)
|
|
{
|
|
info->MinAmmo1 = val;
|
|
}
|
|
else
|
|
{
|
|
Printf (unknown_str, Line1, "Weapon", weapNum);
|
|
}
|
|
}
|
|
|
|
if (info->AmmoType1 == NULL)
|
|
{
|
|
info->AmmoUse1 = 0;
|
|
}
|
|
|
|
if (patchedStates)
|
|
{
|
|
statedef.InstallStates(type, info);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
static void SetPointer(FState *state, PFunction *sym, int frame = 0)
|
|
{
|
|
if (sym == NULL)
|
|
{
|
|
state->ClearAction();
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
state->SetAction(sym->Variants[0].Implementation);
|
|
|
|
for (unsigned int i = 0; i < MBFCodePointers.Size(); i++)
|
|
{
|
|
if (sym->SymbolName == MBFCodePointers[i].name)
|
|
{
|
|
MBFParamState newstate;
|
|
newstate.state = state;
|
|
newstate.pointer = i;
|
|
MBFParamStates.Push(newstate);
|
|
break; // No need to cycle through the rest of the list.
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static int PatchPointer (int ptrNum)
|
|
{
|
|
int result;
|
|
|
|
// Hack for some Boom dehacked patches that are of the form Pointer 0 (x statenumber)
|
|
char * key;
|
|
int indexnum;
|
|
key=strchr(Line2, '(');
|
|
if (key++) key=strchr(key, ' '); else key=NULL;
|
|
if ((ptrNum == 0) && key++)
|
|
{
|
|
*strchr(key, ')') = '\0';
|
|
indexnum = atoi(key);
|
|
for (ptrNum = 0; (unsigned int) ptrNum < CodePConv.Size(); ++ptrNum)
|
|
{
|
|
if (CodePConv[ptrNum] == indexnum) break;
|
|
}
|
|
DPrintf("Final ptrNum: %i\n", ptrNum);
|
|
}
|
|
// End of hack.
|
|
|
|
// 448 Doom states with codepointers + 74 extra MBF states with codepointers = 522 total
|
|
// Better to just use the size of the array rather than a hardcoded value.
|
|
if (ptrNum >= 0 && (unsigned int) ptrNum < CodePConv.Size())
|
|
{
|
|
DPrintf ("Pointer %d\n", ptrNum);
|
|
}
|
|
else
|
|
{
|
|
Printf ("Pointer %d out of range.\n", ptrNum);
|
|
ptrNum = -1;
|
|
}
|
|
|
|
while ((result = GetLine ()) == 1)
|
|
{
|
|
if ((unsigned)ptrNum < CodePConv.Size() && (!stricmp (Line1, "Codep Frame")))
|
|
{
|
|
FState *state = FindState (CodePConv[ptrNum]);
|
|
if (state)
|
|
{
|
|
int index = atoi(Line2);
|
|
if ((unsigned)(index) >= Actions.Size())
|
|
{
|
|
SetPointer(state, NULL);
|
|
}
|
|
else
|
|
{
|
|
SetPointer(state, Actions[index], CodePConv[ptrNum]);
|
|
}
|
|
DPrintf("%s has a hacked state for pointer num %i with index %i\nLine1=%s, Line2=%s\n",
|
|
state->StaticFindStateOwner(state)->TypeName.GetChars(), ptrNum, index, Line1, Line2);
|
|
}
|
|
else
|
|
{
|
|
Printf ("Bad code pointer %d\n", ptrNum);
|
|
}
|
|
}
|
|
else Printf (unknown_str, Line1, "Pointer", ptrNum);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
static int PatchCheats (int dummy)
|
|
{
|
|
int result;
|
|
|
|
DPrintf ("Cheats (support removed by request)\n");
|
|
|
|
while ((result = GetLine ()) == 1)
|
|
{
|
|
}
|
|
return result;
|
|
}
|
|
|
|
static int PatchMisc (int dummy)
|
|
{
|
|
static const struct Key keys[] = {
|
|
{ "Initial Health", static_cast<ptrdiff_t>(myoffsetof(struct DehInfo,StartHealth)) },
|
|
{ "Initial Bullets", static_cast<ptrdiff_t>(myoffsetof(struct DehInfo,StartBullets)) },
|
|
{ "Max Health", static_cast<ptrdiff_t>(myoffsetof(struct DehInfo,MaxHealth)) },
|
|
{ "Max Armor", static_cast<ptrdiff_t>(myoffsetof(struct DehInfo,MaxArmor)) },
|
|
{ "Green Armor Class", static_cast<ptrdiff_t>(myoffsetof(struct DehInfo,GreenAC)) },
|
|
{ "Blue Armor Class", static_cast<ptrdiff_t>(myoffsetof(struct DehInfo,BlueAC)) },
|
|
{ "Max Soulsphere", static_cast<ptrdiff_t>(myoffsetof(struct DehInfo,MaxSoulsphere)) },
|
|
{ "Soulsphere Health", static_cast<ptrdiff_t>(myoffsetof(struct DehInfo,SoulsphereHealth)) },
|
|
{ "Megasphere Health", static_cast<ptrdiff_t>(myoffsetof(struct DehInfo,MegasphereHealth)) },
|
|
{ "God Mode Health", static_cast<ptrdiff_t>(myoffsetof(struct DehInfo,GodHealth)) },
|
|
{ "IDFA Armor", static_cast<ptrdiff_t>(myoffsetof(struct DehInfo,FAArmor)) },
|
|
{ "IDFA Armor Class", static_cast<ptrdiff_t>(myoffsetof(struct DehInfo,FAAC)) },
|
|
{ "IDKFA Armor", static_cast<ptrdiff_t>(myoffsetof(struct DehInfo,KFAArmor)) },
|
|
{ "IDKFA Armor Class", static_cast<ptrdiff_t>(myoffsetof(struct DehInfo,KFAAC)) },
|
|
{ "No Autofreeze", static_cast<ptrdiff_t>(myoffsetof(struct DehInfo,NoAutofreeze)) },
|
|
{ NULL, 0 }
|
|
};
|
|
int result;
|
|
|
|
DPrintf ("Misc\n");
|
|
|
|
while ((result = GetLine()) == 1)
|
|
{
|
|
if (HandleKey (keys, &deh, Line1, atoi (Line2)))
|
|
{
|
|
if (stricmp (Line1, "BFG Cells/Shot") == 0)
|
|
{
|
|
deh.BFGCells = atoi (Line2);
|
|
}
|
|
else if (stricmp (Line1, "Rocket Explosion Style") == 0)
|
|
{
|
|
stripwhite (Line2);
|
|
int style = FindStyle (Line2);
|
|
if (style >= 0)
|
|
{
|
|
deh.ExplosionStyle = style;
|
|
}
|
|
}
|
|
else if (stricmp (Line1, "Rocket Explosion Alpha") == 0)
|
|
{
|
|
deh.ExplosionAlpha = atof (Line2);
|
|
}
|
|
else if (stricmp (Line1, "Monsters Infight") == 0)
|
|
{
|
|
infighting = atoi (Line2);
|
|
}
|
|
else if (stricmp (Line1, "Monsters Ignore Each Other") == 0)
|
|
{
|
|
infighting = atoi (Line2) ? -1 : 0;
|
|
}
|
|
else if (strnicmp (Line1, "Powerup Color ", 14) == 0)
|
|
{
|
|
static const char * const names[] =
|
|
{
|
|
"Invulnerability",
|
|
"Berserk",
|
|
"Invisibility",
|
|
"Radiation Suit",
|
|
"Infrared",
|
|
"Tome of Power",
|
|
"Wings of Wrath",
|
|
"Speed",
|
|
"Minotaur",
|
|
NULL
|
|
};
|
|
static const PClass * const *types[] =
|
|
{
|
|
&RUNTIME_CLASS_CASTLESS(APowerInvulnerable),
|
|
&RUNTIME_CLASS_CASTLESS(APowerStrength),
|
|
&RUNTIME_CLASS_CASTLESS(APowerInvisibility),
|
|
&RUNTIME_CLASS_CASTLESS(APowerIronFeet),
|
|
&RUNTIME_CLASS_CASTLESS(APowerLightAmp),
|
|
&RUNTIME_CLASS_CASTLESS(APowerWeaponLevel2),
|
|
&RUNTIME_CLASS_CASTLESS(APowerSpeed),
|
|
&RUNTIME_CLASS_CASTLESS(APowerMinotaur)
|
|
};
|
|
int i;
|
|
|
|
for (i = 0; names[i] != NULL; ++i)
|
|
{
|
|
if (stricmp (Line1 + 14, names[i]) == 0)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
if (names[i] == NULL)
|
|
{
|
|
Printf ("Unknown miscellaneous info %s.\n", Line1);
|
|
}
|
|
else
|
|
{
|
|
int r, g, b;
|
|
float a;
|
|
|
|
if (4 != sscanf (Line2, "%d %d %d %f", &r, &g, &b, &a))
|
|
{
|
|
Printf ("Bad powerup color description \"%s\" for %s\n", Line2, Line1);
|
|
}
|
|
else if (a > 0)
|
|
{
|
|
static_cast<APowerup *>(GetDefaultByType (*types[i]))->BlendColor = PalEntry(
|
|
BYTE(clamp(a,0.f,1.f)*255.f),
|
|
clamp(r,0,255),
|
|
clamp(g,0,255),
|
|
clamp(b,0,255));
|
|
}
|
|
else
|
|
{
|
|
static_cast<APowerup *>(GetDefaultByType (*types[i]))->BlendColor = 0;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Printf ("Unknown miscellaneous info %s.\n", Line1);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update default item properties by patching the affected items
|
|
// Note: This won't have any effect on DECORATE derivates of these items!
|
|
ABasicArmorPickup *armor;
|
|
|
|
armor = static_cast<ABasicArmorPickup *> (GetDefaultByName ("GreenArmor"));
|
|
if (armor!=NULL)
|
|
{
|
|
armor->SaveAmount = 100 * deh.GreenAC;
|
|
armor->SavePercent = deh.GreenAC == 1 ? 0.33335 : 0.5;
|
|
}
|
|
armor = static_cast<ABasicArmorPickup *> (GetDefaultByName ("BlueArmor"));
|
|
if (armor!=NULL)
|
|
{
|
|
armor->SaveAmount = 100 * deh.BlueAC;
|
|
armor->SavePercent = deh.BlueAC == 1 ? 0.33335 : 0.5;
|
|
}
|
|
|
|
ABasicArmorBonus *barmor;
|
|
barmor = static_cast<ABasicArmorBonus *> (GetDefaultByName ("ArmorBonus"));
|
|
if (barmor!=NULL)
|
|
{
|
|
barmor->MaxSaveAmount = deh.MaxArmor;
|
|
}
|
|
|
|
AHealth *health;
|
|
health = static_cast<AHealth *> (GetDefaultByName ("HealthBonus"));
|
|
if (health!=NULL)
|
|
{
|
|
health->MaxAmount = 2 * deh.MaxHealth;
|
|
}
|
|
|
|
health = static_cast<AHealth *> (GetDefaultByName ("Soulsphere"));
|
|
if (health!=NULL)
|
|
{
|
|
health->Amount = deh.SoulsphereHealth;
|
|
health->MaxAmount = deh.MaxSoulsphere;
|
|
}
|
|
|
|
health = static_cast<AHealth *> (GetDefaultByName ("MegasphereHealth"));
|
|
if (health!=NULL)
|
|
{
|
|
health->Amount = health->MaxAmount = deh.MegasphereHealth;
|
|
}
|
|
|
|
APlayerPawn *player = static_cast<APlayerPawn *> (GetDefaultByName ("DoomPlayer"));
|
|
if (player != NULL)
|
|
{
|
|
player->health = deh.StartHealth;
|
|
|
|
// Hm... I'm not sure that this is the right way to change this info...
|
|
DDropItem *di = PClass::FindActor(NAME_DoomPlayer)->DropItems;
|
|
while (di != NULL)
|
|
{
|
|
if (di->Name == NAME_Clip)
|
|
{
|
|
di->Amount = deh.StartBullets;
|
|
}
|
|
di = di->Next;
|
|
}
|
|
}
|
|
|
|
|
|
// 0xDD means "enable infighting"
|
|
if (infighting == 0xDD)
|
|
{
|
|
infighting = 1;
|
|
}
|
|
else if (infighting != -1)
|
|
{
|
|
infighting = 0;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
static int PatchPars (int dummy)
|
|
{
|
|
char *space, mapname[8], *moredata;
|
|
level_info_t *info;
|
|
int result, par;
|
|
|
|
DPrintf ("[Pars]\n");
|
|
|
|
while ( (result = GetLine()) ) {
|
|
// Argh! .bex doesn't follow the same rules as .deh
|
|
if (result == 1) {
|
|
Printf ("Unknown key in [PARS] section: %s\n", Line1);
|
|
continue;
|
|
}
|
|
if (stricmp ("par", Line1))
|
|
return result;
|
|
|
|
space = strchr (Line2, ' ');
|
|
|
|
if (!space) {
|
|
Printf ("Need data after par.\n");
|
|
continue;
|
|
}
|
|
|
|
*space++ = '\0';
|
|
|
|
while (*space && isspace(*space))
|
|
space++;
|
|
|
|
moredata = strchr (space, ' ');
|
|
|
|
if (moredata) {
|
|
// At least 3 items on this line, must be E?M? format
|
|
mysnprintf (mapname, countof(mapname), "E%cM%c", *Line2, *space);
|
|
par = atoi (moredata + 1);
|
|
} else {
|
|
// Only 2 items, must be MAP?? format
|
|
mysnprintf (mapname, countof(mapname), "MAP%02d", atoi(Line2) % 100);
|
|
par = atoi (space);
|
|
}
|
|
|
|
if (!(info = FindLevelInfo (mapname)) ) {
|
|
Printf ("No map %s\n", mapname);
|
|
continue;
|
|
}
|
|
|
|
info->partime = par;
|
|
DPrintf ("Par for %s changed to %d\n", mapname, par);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
static int PatchCodePtrs (int dummy)
|
|
{
|
|
int result;
|
|
|
|
DPrintf ("[CodePtr]\n");
|
|
|
|
while ((result = GetLine()) == 1)
|
|
{
|
|
if (!strnicmp ("Frame", Line1, 5) && isspace(Line1[5]))
|
|
{
|
|
int frame = atoi (Line1 + 5);
|
|
FState *state = FindState (frame);
|
|
|
|
stripwhite (Line2);
|
|
if (state == NULL)
|
|
{
|
|
Printf ("Frame %d out of range\n", frame);
|
|
}
|
|
else if (!stricmp(Line2, "NULL"))
|
|
{
|
|
SetPointer(state, NULL);
|
|
}
|
|
else
|
|
{
|
|
FString symname;
|
|
|
|
|
|
if ((Line2[0] == 'A' || Line2[0] == 'a') && Line2[1] == '_')
|
|
symname = Line2;
|
|
else
|
|
symname.Format("A_%s", Line2);
|
|
|
|
// Let's consider as aliases some redundant MBF pointer
|
|
for (unsigned int i = 0; i < MBFCodePointers.Size(); i++)
|
|
{
|
|
if (!symname.CompareNoCase(MBFCodePointers[i].alias))
|
|
{
|
|
symname = MBFCodePointers[i].name;
|
|
Printf("%s --> %s\n", MBFCodePointers[i].alias, MBFCodePointers[i].name.GetChars());
|
|
}
|
|
}
|
|
|
|
// This skips the action table and goes directly to the internal symbol table
|
|
// DEH compatible functions are easy to recognize.
|
|
PFunction *sym = dyn_cast<PFunction>(RUNTIME_CLASS(AInventory)->Symbols.FindSymbol(symname, true));
|
|
if (sym == NULL)
|
|
{
|
|
Printf("Frame %d: Unknown code pointer '%s'\n", frame, Line2);
|
|
}
|
|
else
|
|
{
|
|
TArray<DWORD> &args = sym->Variants[0].ArgFlags;
|
|
if ((sym->Flags & (VARF_Method | VARF_Action)) != (VARF_Method | VARF_Action) || (args.Size() > NAP && !(args[NAP] & VARF_Optional)))
|
|
{
|
|
Printf("Frame %d: Incompatible code pointer '%s'\n", frame, Line2);
|
|
sym = NULL;
|
|
}
|
|
}
|
|
SetPointer(state, sym, frame);
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
static int PatchMusic (int dummy)
|
|
{
|
|
int result;
|
|
|
|
DPrintf ("[Music]\n");
|
|
|
|
while ((result = GetLine()) == 1)
|
|
{
|
|
const char *newname = skipwhite (Line2);
|
|
FString keystring;
|
|
|
|
keystring << "MUSIC_" << Line1;
|
|
|
|
GStrings.SetString (keystring, newname);
|
|
DPrintf ("Music %s set to:\n%s\n", keystring.GetChars(), newname);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
static int PatchText (int oldSize)
|
|
{
|
|
int newSize;
|
|
char *oldStr;
|
|
char *newStr;
|
|
char *temp;
|
|
INTBOOL good;
|
|
int result;
|
|
int i;
|
|
|
|
// Skip old size, since we already know it
|
|
temp = Line2;
|
|
while (*temp > ' ')
|
|
temp++;
|
|
while (*temp && *temp <= ' ')
|
|
temp++;
|
|
|
|
if (*temp == 0)
|
|
{
|
|
Printf ("Text chunk is missing size of new string.\n");
|
|
return 2;
|
|
}
|
|
newSize = atoi (temp);
|
|
|
|
oldStr = new char[oldSize + 1];
|
|
newStr = new char[newSize + 1];
|
|
|
|
if (!oldStr || !newStr)
|
|
{
|
|
Printf ("Out of memory.\n");
|
|
goto donewithtext;
|
|
}
|
|
|
|
good = ReadChars (&oldStr, oldSize);
|
|
good += ReadChars (&newStr, newSize);
|
|
|
|
if (!good)
|
|
{
|
|
delete[] newStr;
|
|
delete[] oldStr;
|
|
Printf ("Unexpected end-of-file.\n");
|
|
return 0;
|
|
}
|
|
|
|
if (includenotext)
|
|
{
|
|
Printf ("Skipping text chunk in included patch.\n");
|
|
goto donewithtext;
|
|
}
|
|
|
|
DPrintf ("Searching for text:\n%s\n", oldStr);
|
|
good = false;
|
|
|
|
// Search through sprite names; they are always 4 chars
|
|
if (oldSize == 4)
|
|
{
|
|
i = FindSprite (oldStr);
|
|
if (i != -1)
|
|
{
|
|
strncpy (sprites[i].name, newStr, 4);
|
|
if (strncmp ("PLAY", oldStr, 4) == 0)
|
|
{
|
|
strncpy (deh.PlayerSprite, newStr, 4);
|
|
}
|
|
for (unsigned ii = 0; ii < OrgSprNames.Size(); ii++)
|
|
{
|
|
if (!stricmp(OrgSprNames[ii].c, oldStr))
|
|
{
|
|
strcpy(OrgSprNames[ii].c, newStr);
|
|
}
|
|
}
|
|
// If this sprite is used by a pickup, then the DehackedPickup sprite map
|
|
// needs to be updated too.
|
|
for (i = 0; (size_t)i < countof(DehSpriteMappings); ++i)
|
|
{
|
|
if (strncmp (DehSpriteMappings[i].Sprite, oldStr, 4) == 0)
|
|
{
|
|
// Found a match, so change it.
|
|
strncpy (DehSpriteMappings[i].Sprite, newStr, 4);
|
|
|
|
// Now shift the map's entries around so that it stays sorted.
|
|
// This must be done because the map is scanned using a binary search.
|
|
while (i > 0 && strncmp (DehSpriteMappings[i-1].Sprite, newStr, 4) > 0)
|
|
{
|
|
swapvalues (DehSpriteMappings[i-1], DehSpriteMappings[i]);
|
|
--i;
|
|
}
|
|
while ((size_t)i < countof(DehSpriteMappings)-1 &&
|
|
strncmp (DehSpriteMappings[i+1].Sprite, newStr, 4) < 0)
|
|
{
|
|
swapvalues (DehSpriteMappings[i+1], DehSpriteMappings[i]);
|
|
++i;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
goto donewithtext;
|
|
}
|
|
}
|
|
|
|
// Search through most other texts
|
|
const char *str;
|
|
do
|
|
{
|
|
str = EnglishStrings->MatchString(oldStr);
|
|
if (str != NULL)
|
|
{
|
|
GStrings.SetString(str, newStr);
|
|
EnglishStrings->SetString(str, "~~"); // set to something invalid so that it won't get found again by the next iteration or by another replacement later
|
|
good = true;
|
|
}
|
|
}
|
|
while (str != NULL); // repeat search until the text can no longer be found
|
|
|
|
if (!good)
|
|
{
|
|
DPrintf (" (Unmatched)\n");
|
|
}
|
|
|
|
donewithtext:
|
|
if (newStr)
|
|
delete[] newStr;
|
|
if (oldStr)
|
|
delete[] oldStr;
|
|
|
|
// Fetch next identifier for main loop
|
|
while ((result = GetLine ()) == 1)
|
|
;
|
|
|
|
return result;
|
|
}
|
|
|
|
static int PatchStrings (int dummy)
|
|
{
|
|
int result;
|
|
|
|
DPrintf ("[Strings]\n");
|
|
|
|
while ((result = GetLine()) == 1)
|
|
{
|
|
FString holdstring;
|
|
do
|
|
{
|
|
holdstring += skipwhite (Line2);
|
|
holdstring.StripRight();
|
|
if (holdstring.Len() > 0 && holdstring[holdstring.Len()-1] == '\\')
|
|
{
|
|
holdstring.Truncate((long)holdstring.Len()-1);
|
|
Line2 = igets ();
|
|
}
|
|
else
|
|
{
|
|
Line2 = NULL;
|
|
}
|
|
} while (Line2 && *Line2);
|
|
|
|
ReplaceSpecialChars (holdstring.LockBuffer());
|
|
holdstring.UnlockBuffer();
|
|
// Account for a discrepancy between Boom's and ZDoom's name for the red skull key pickup message
|
|
const char *ll = Line1;
|
|
if (!stricmp(ll, "GOTREDSKULL")) ll = "GOTREDSKUL";
|
|
GStrings.SetString (ll, holdstring);
|
|
DPrintf ("%s set to:\n%s\n", Line1, holdstring.GetChars());
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
static int DoInclude (int dummy)
|
|
{
|
|
char *data;
|
|
int savedversion, savepversion, savepatchsize;
|
|
char *savepatchfile, *savepatchpt, *savepatchname;
|
|
|
|
if (including)
|
|
{
|
|
Printf ("Sorry, can't nest includes\n");
|
|
return GetLine();
|
|
}
|
|
|
|
if (strnicmp (Line2, "notext", 6) == 0 && Line2[6] != 0 && isspace(Line2[6]))
|
|
{
|
|
includenotext = true;
|
|
Line2 = skipwhite (Line2+7);
|
|
}
|
|
|
|
stripwhite (Line2);
|
|
if (*Line2 == '\"')
|
|
{
|
|
data = ++Line2;
|
|
while (*data && *data != '\"')
|
|
data++;
|
|
*data = 0;
|
|
}
|
|
|
|
if (*Line2 == 0)
|
|
{
|
|
Printf ("Include directive is missing filename\n");
|
|
}
|
|
else
|
|
{
|
|
data = Line2;
|
|
DPrintf ("Including %s\n", data);
|
|
savepatchname = PatchName;
|
|
savepatchfile = PatchFile;
|
|
savepatchpt = PatchPt;
|
|
savepatchsize = PatchSize;
|
|
savedversion = dversion;
|
|
savepversion = pversion;
|
|
including = true;
|
|
|
|
// Try looking for the included file in the same directory
|
|
// as the patch before looking in the current file.
|
|
const char *lastSlash = savepatchname ? strrchr (savepatchname, '/') : NULL;
|
|
char *path = data;
|
|
|
|
if (lastSlash != NULL)
|
|
{
|
|
size_t pathlen = lastSlash - savepatchname + strlen (data) + 2;
|
|
path = new char[pathlen];
|
|
strncpy (path, savepatchname, (lastSlash - savepatchname) + 1);
|
|
strcpy (path + (lastSlash - savepatchname) + 1, data);
|
|
if (!FileExists (path))
|
|
{
|
|
delete[] path;
|
|
path = data;
|
|
}
|
|
}
|
|
|
|
D_LoadDehFile(path);
|
|
|
|
if (data != path)
|
|
{
|
|
delete[] path;
|
|
}
|
|
|
|
DPrintf ("Done with include\n");
|
|
PatchName = savepatchname;
|
|
PatchFile = savepatchfile;
|
|
PatchPt = savepatchpt;
|
|
PatchSize = savepatchsize;
|
|
dversion = savedversion;
|
|
pversion = savepversion;
|
|
}
|
|
|
|
including = false;
|
|
includenotext = false;
|
|
return GetLine();
|
|
}
|
|
|
|
CVAR(Int, dehload, 0, CVAR_ARCHIVE) // Autoloading of .DEH lumps is disabled by default.
|
|
|
|
// checks if lump is a .deh or .bex file. Only lumps in the root directory are considered valid.
|
|
static bool isDehFile(int lumpnum)
|
|
{
|
|
const char* const fullName = Wads.GetLumpFullName(lumpnum);
|
|
const char* const extension = strrchr(fullName, '.');
|
|
|
|
return NULL != extension && strchr(fullName, '/') == NULL
|
|
&& (0 == stricmp(extension, ".deh") || 0 == stricmp(extension, ".bex"));
|
|
}
|
|
|
|
int D_LoadDehLumps()
|
|
{
|
|
int lastlump = 0, lumpnum, count = 0;
|
|
|
|
while ((lumpnum = Wads.FindLump("DEHACKED", &lastlump)) >= 0)
|
|
{
|
|
count += D_LoadDehLump(lumpnum);
|
|
}
|
|
|
|
if (0 == PatchSize && dehload > 0)
|
|
{
|
|
// No DEH/BEX patch is loaded yet, try to find lump(s) with specific extensions
|
|
|
|
if (dehload == 1) // load all .DEH lumps that are found.
|
|
{
|
|
for (lumpnum = 0, lastlump = Wads.GetNumLumps(); lumpnum < lastlump; ++lumpnum)
|
|
{
|
|
if (isDehFile(lumpnum))
|
|
{
|
|
count += D_LoadDehLump(lumpnum);
|
|
}
|
|
}
|
|
}
|
|
else // only load the last .DEH lump that is found.
|
|
{
|
|
for (lumpnum = Wads.GetNumLumps()-1; lumpnum >=0; --lumpnum)
|
|
{
|
|
if (isDehFile(lumpnum))
|
|
{
|
|
count += D_LoadDehLump(lumpnum);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
bool D_LoadDehLump(int lumpnum)
|
|
{
|
|
PatchSize = Wads.LumpLength(lumpnum);
|
|
|
|
PatchName = copystring(Wads.GetLumpFullPath(lumpnum));
|
|
PatchFile = new char[PatchSize + 1];
|
|
Wads.ReadLump(lumpnum, PatchFile);
|
|
PatchFile[PatchSize] = '\0'; // terminate with a '\0' character
|
|
return DoDehPatch();
|
|
}
|
|
|
|
bool D_LoadDehFile(const char *patchfile)
|
|
{
|
|
FILE *deh;
|
|
|
|
deh = fopen(patchfile, "rb");
|
|
if (deh != NULL)
|
|
{
|
|
PatchSize = Q_filelength(deh);
|
|
|
|
PatchName = copystring(patchfile);
|
|
PatchFile = new char[PatchSize + 1];
|
|
fread(PatchFile, 1, PatchSize, deh);
|
|
fclose(deh);
|
|
PatchFile[PatchSize] = '\0'; // terminate with a '\0' character
|
|
return DoDehPatch();
|
|
}
|
|
else
|
|
{
|
|
// Couldn't find it in the filesystem; try from a lump instead.
|
|
int lumpnum = Wads.CheckNumForFullName(patchfile, true);
|
|
if (lumpnum < 0)
|
|
{
|
|
// Compatibility fallback. It's just here because
|
|
// some WAD may need it. Should be deleted if it can
|
|
// be confirmed that nothing uses this case.
|
|
FString filebase(ExtractFileBase(patchfile));
|
|
lumpnum = Wads.CheckNumForName(filebase);
|
|
}
|
|
if (lumpnum >= 0)
|
|
{
|
|
return D_LoadDehLump(lumpnum);
|
|
}
|
|
}
|
|
Printf ("Could not open DeHackEd patch \"%s\"\n", patchfile);
|
|
return false;
|
|
}
|
|
|
|
static bool DoDehPatch()
|
|
{
|
|
if (!batchrun) Printf("Adding dehacked patch %s\n", PatchName);
|
|
|
|
int cont;
|
|
|
|
dversion = pversion = -1;
|
|
cont = 0;
|
|
if (0 == strncmp (PatchFile, "Patch File for DeHackEd v", 25))
|
|
{
|
|
if (PatchFile[25] < '3' && PatchFile[25] != '2' && PatchFile[27] != '3')
|
|
{
|
|
Printf (PRINT_BOLD, "\"%s\" is an old and unsupported DeHackEd patch\n", PatchName);
|
|
delete[] PatchName;
|
|
delete[] PatchFile;
|
|
return false;
|
|
}
|
|
// fix for broken WolfenDoom patches which contain \0 characters in some places.
|
|
for (int i = 0; i < PatchSize; i++)
|
|
{
|
|
if (PatchFile[i] == 0) PatchFile[i] = ' ';
|
|
}
|
|
|
|
PatchPt = strchr (PatchFile, '\n');
|
|
while ((cont = GetLine()) == 1)
|
|
{
|
|
CHECKKEY ("Doom version", dversion)
|
|
else CHECKKEY ("Patch format", pversion)
|
|
}
|
|
if (!cont || dversion == -1 || pversion == -1)
|
|
{
|
|
delete[] PatchName;
|
|
delete[] PatchFile;
|
|
Printf (PRINT_BOLD, "\"%s\" is not a DeHackEd patch file\n", PatchFile);
|
|
return false;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
DPrintf ("Patch does not have DeHackEd signature. Assuming .bex\n");
|
|
dversion = 19;
|
|
pversion = 6;
|
|
PatchPt = PatchFile;
|
|
while ((cont = GetLine()) == 1)
|
|
{}
|
|
}
|
|
|
|
if (pversion != 5 && pversion != 6)
|
|
{
|
|
Printf ("DeHackEd patch version is %d.\nUnexpected results may occur.\n", pversion);
|
|
}
|
|
|
|
if (dversion == 16)
|
|
dversion = 0;
|
|
else if (dversion == 17)
|
|
dversion = 2;
|
|
else if (dversion == 19)
|
|
dversion = 3;
|
|
else if (dversion == 20)
|
|
dversion = 1;
|
|
else if (dversion == 21)
|
|
dversion = 4;
|
|
else
|
|
{
|
|
Printf ("Patch created with unknown DOOM version.\nAssuming version 1.9.\n");
|
|
dversion = 3;
|
|
}
|
|
|
|
if (!LoadDehSupp ())
|
|
{
|
|
Printf ("Could not load DEH support data\n");
|
|
UnloadDehSupp ();
|
|
delete[] PatchName;
|
|
delete[] PatchFile;
|
|
return false;
|
|
}
|
|
|
|
do
|
|
{
|
|
if (cont == 1)
|
|
{
|
|
Printf ("Key %s encountered out of context\n", Line1);
|
|
cont = 0;
|
|
}
|
|
else if (cont == 2)
|
|
{
|
|
cont = HandleMode (Line1, atoi (Line2));
|
|
}
|
|
} while (cont);
|
|
|
|
UnloadDehSupp ();
|
|
delete[] PatchName;
|
|
delete[] PatchFile;
|
|
if (!batchrun) Printf ("Patch installed\n");
|
|
return true;
|
|
}
|
|
|
|
static inline bool CompareLabel (const char *want, const BYTE *have)
|
|
{
|
|
return *(DWORD *)want == *(DWORD *)have;
|
|
}
|
|
|
|
static int DehUseCount;
|
|
|
|
static void UnloadDehSupp ()
|
|
{
|
|
if (--DehUseCount <= 0)
|
|
{
|
|
// Handle MBF params here, before the required arrays are cleared
|
|
for (unsigned int i=0; i < MBFParamStates.Size(); i++)
|
|
{
|
|
SetDehParams(MBFParamStates[i].state, MBFParamStates[i].pointer);
|
|
}
|
|
MBFParamStates.Clear();
|
|
MBFParamStates.ShrinkToFit();
|
|
MBFCodePointers.Clear();
|
|
MBFCodePointers.ShrinkToFit();
|
|
// StateMap is not freed here, because if you load a second
|
|
// dehacked patch through some means other than including it
|
|
// in the first patch, it won't see the state information
|
|
// that was altered by the first. So we need to keep the
|
|
// StateMap around until all patches have been applied.
|
|
DehUseCount = 0;
|
|
Actions.Clear();
|
|
Actions.ShrinkToFit();
|
|
OrgHeights.Clear();
|
|
OrgHeights.ShrinkToFit();
|
|
CodePConv.Clear();
|
|
CodePConv.ShrinkToFit();
|
|
OrgSprNames.Clear();
|
|
OrgSprNames.ShrinkToFit();
|
|
SoundMap.Clear();
|
|
SoundMap.ShrinkToFit();
|
|
InfoNames.Clear();
|
|
InfoNames.ShrinkToFit();
|
|
BitNames.Clear();
|
|
BitNames.ShrinkToFit();
|
|
StyleNames.Clear();
|
|
StyleNames.ShrinkToFit();
|
|
AmmoNames.Clear();
|
|
AmmoNames.ShrinkToFit();
|
|
|
|
if (UnchangedSpriteNames != NULL)
|
|
{
|
|
delete[] UnchangedSpriteNames;
|
|
UnchangedSpriteNames = NULL;
|
|
NumUnchangedSprites = 0;
|
|
}
|
|
if (EnglishStrings != NULL)
|
|
{
|
|
delete EnglishStrings;
|
|
EnglishStrings = NULL;
|
|
}
|
|
}
|
|
}
|
|
|
|
static bool LoadDehSupp ()
|
|
{
|
|
try
|
|
{
|
|
// Make sure we only get the DEHSUPP lump from zdoom.pk3
|
|
// User modifications are not supported!
|
|
int lump = Wads.CheckNumForName("DEHSUPP");
|
|
|
|
if (lump == -1)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (Wads.GetLumpFile(lump) > 0)
|
|
{
|
|
Printf("Warning: DEHSUPP no longer supported. DEHACKED patch disabled.\n");
|
|
return false;
|
|
}
|
|
bool gotnames = false;
|
|
int i;
|
|
|
|
|
|
if (++DehUseCount > 1)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (EnglishStrings == NULL)
|
|
{
|
|
EnglishStrings = new FStringTable;
|
|
EnglishStrings->LoadStrings (true);
|
|
}
|
|
|
|
if (UnchangedSpriteNames == NULL)
|
|
{
|
|
UnchangedSpriteNames = new char[sprites.Size()*4];
|
|
NumUnchangedSprites = sprites.Size();
|
|
for (i = 0; i < NumUnchangedSprites; ++i)
|
|
{
|
|
memcpy (UnchangedSpriteNames+i*4, &sprites[i].name, 4);
|
|
}
|
|
}
|
|
|
|
FScanner sc;
|
|
|
|
sc.OpenLumpNum(lump);
|
|
sc.SetCMode(true);
|
|
|
|
while (sc.GetString())
|
|
{
|
|
if (sc.Compare("ActionList"))
|
|
{
|
|
sc.MustGetStringName("{");
|
|
while (!sc.CheckString("}"))
|
|
{
|
|
sc.MustGetString();
|
|
if (sc.Compare("NULL"))
|
|
{
|
|
Actions.Push(NULL);
|
|
}
|
|
else
|
|
{
|
|
// all relevant code pointers are either defined in AInventory
|
|
// or AActor so this will find all of them.
|
|
FString name = "A_";
|
|
name << sc.String;
|
|
PFunction *sym = dyn_cast<PFunction>(RUNTIME_CLASS(AInventory)->Symbols.FindSymbol(name, true));
|
|
if (sym == NULL)
|
|
{
|
|
sc.ScriptError("Unknown code pointer '%s'", sc.String);
|
|
}
|
|
else
|
|
{
|
|
TArray<DWORD> &args = sym->Variants[0].ArgFlags;
|
|
if ((sym->Flags & (VARF_Method|VARF_Action)) != (VARF_Method | VARF_Action) || (args.Size() > NAP && !(args[NAP] & VARF_Optional)))
|
|
{
|
|
sc.ScriptMessage("Incompatible code pointer '%s'", sc.String);
|
|
}
|
|
}
|
|
Actions.Push(sym);
|
|
}
|
|
if (sc.CheckString("}")) break;
|
|
sc.MustGetStringName(",");
|
|
}
|
|
}
|
|
else if (sc.Compare("OrgHeights"))
|
|
{
|
|
sc.MustGetStringName("{");
|
|
while (!sc.CheckString("}"))
|
|
{
|
|
sc.MustGetNumber();
|
|
OrgHeights.Push(sc.Number);
|
|
if (sc.CheckString("}")) break;
|
|
sc.MustGetStringName(",");
|
|
}
|
|
}
|
|
else if (sc.Compare("CodePConv"))
|
|
{
|
|
sc.MustGetStringName("{");
|
|
while (!sc.CheckString("}"))
|
|
{
|
|
sc.MustGetNumber();
|
|
CodePConv.Push(sc.Number);
|
|
if (sc.CheckString("}")) break;
|
|
sc.MustGetStringName(",");
|
|
}
|
|
}
|
|
else if (sc.Compare("OrgSprNames"))
|
|
{
|
|
sc.MustGetStringName("{");
|
|
while (!sc.CheckString("}"))
|
|
{
|
|
sc.MustGetString();
|
|
DEHSprName s;
|
|
// initialize with zeroes
|
|
memset(&s, 0, sizeof(s));
|
|
if (strlen(sc.String) ==4)
|
|
{
|
|
s.c[0] = sc.String[0];
|
|
s.c[1] = sc.String[1];
|
|
s.c[2] = sc.String[2];
|
|
s.c[3] = sc.String[3];
|
|
s.c[4] = 0;
|
|
}
|
|
else
|
|
{
|
|
sc.ScriptError("Invalid sprite name '%s' (must be 4 characters)", sc.String);
|
|
}
|
|
OrgSprNames.Push(s);
|
|
if (sc.CheckString("}")) break;
|
|
sc.MustGetStringName(",");
|
|
}
|
|
}
|
|
else if (sc.Compare("StateMap"))
|
|
{
|
|
bool addit = StateMap.Size() == 0;
|
|
|
|
sc.MustGetStringName("{");
|
|
while (!sc.CheckString("}"))
|
|
{
|
|
StateMapper s;
|
|
sc.MustGetString();
|
|
|
|
PClass *type = PClass::FindClass (sc.String);
|
|
if (type == NULL)
|
|
{
|
|
sc.ScriptError ("Can't find type %s", sc.String);
|
|
}
|
|
else if (!type->IsKindOf(RUNTIME_CLASS(PClassActor)))
|
|
{
|
|
sc.ScriptError ("%s is not an actor", sc.String);
|
|
}
|
|
|
|
sc.MustGetStringName(",");
|
|
sc.MustGetString();
|
|
PClassActor *actortype = static_cast<PClassActor *>(type);
|
|
s.State = actortype->FindState(sc.String);
|
|
if (s.State == NULL)
|
|
{
|
|
sc.ScriptError("Invalid state '%s' in '%s'", sc.String, type->TypeName.GetChars());
|
|
}
|
|
|
|
sc.MustGetStringName(",");
|
|
sc.MustGetNumber();
|
|
if (s.State == NULL || s.State + sc.Number > actortype->OwnedStates + actortype->NumOwnedStates)
|
|
{
|
|
sc.ScriptError("Invalid state range in '%s'", type->TypeName.GetChars());
|
|
}
|
|
AActor *def = GetDefaultByType(type);
|
|
|
|
s.StateSpan = sc.Number;
|
|
s.Owner = actortype;
|
|
s.OwnerIsPickup = def != NULL && (def->flags & MF_SPECIAL) != 0;
|
|
if (addit) StateMap.Push(s);
|
|
|
|
if (sc.CheckString("}")) break;
|
|
sc.MustGetStringName(",");
|
|
}
|
|
}
|
|
else if (sc.Compare("SoundMap"))
|
|
{
|
|
sc.MustGetStringName("{");
|
|
while (!sc.CheckString("}"))
|
|
{
|
|
sc.MustGetString();
|
|
SoundMap.Push(S_FindSound(sc.String));
|
|
if (sc.CheckString("}")) break;
|
|
sc.MustGetStringName(",");
|
|
}
|
|
}
|
|
else if (sc.Compare("InfoNames"))
|
|
{
|
|
sc.MustGetStringName("{");
|
|
while (!sc.CheckString("}"))
|
|
{
|
|
sc.MustGetString();
|
|
PClassActor *cls = PClass::FindActor(sc.String);
|
|
if (cls == NULL)
|
|
{
|
|
sc.ScriptError("Unknown actor type '%s'", sc.String);
|
|
}
|
|
InfoNames.Push(cls);
|
|
if (sc.CheckString("}")) break;
|
|
sc.MustGetStringName(",");
|
|
}
|
|
}
|
|
else if (sc.Compare("ThingBits"))
|
|
{
|
|
sc.MustGetStringName("{");
|
|
while (!sc.CheckString("}"))
|
|
{
|
|
BitName bit;
|
|
sc.MustGetNumber();
|
|
if (sc.Number < 0 || sc.Number > 31)
|
|
{
|
|
sc.ScriptError("Invalid bit value %d", sc.Number);
|
|
}
|
|
bit.Bit = sc.Number;
|
|
sc.MustGetStringName(",");
|
|
sc.MustGetNumber();
|
|
if (sc.Number < 0 || sc.Number > 2)
|
|
{
|
|
sc.ScriptError("Invalid flag word %d", sc.Number);
|
|
}
|
|
bit.WhichFlags = sc.Number;
|
|
sc.MustGetStringName(",");
|
|
sc.MustGetString();
|
|
strncpy(bit.Name, sc.String, 19);
|
|
bit.Name[19]=0;
|
|
BitNames.Push(bit);
|
|
if (sc.CheckString("}")) break;
|
|
sc.MustGetStringName(",");
|
|
}
|
|
}
|
|
else if (sc.Compare("RenderStyles"))
|
|
{
|
|
sc.MustGetStringName("{");
|
|
while (!sc.CheckString("}"))
|
|
{
|
|
StyleName style;
|
|
sc.MustGetNumber();
|
|
style.Num = sc.Number;
|
|
sc.MustGetStringName(",");
|
|
sc.MustGetString();
|
|
strncpy(style.Name, sc.String, 19);
|
|
style.Name[19]=0;
|
|
StyleNames.Push(style);
|
|
if (sc.CheckString("}")) break;
|
|
sc.MustGetStringName(",");
|
|
}
|
|
}
|
|
else if (sc.Compare("AmmoNames"))
|
|
{
|
|
sc.MustGetStringName("{");
|
|
while (!sc.CheckString("}"))
|
|
{
|
|
sc.MustGetString();
|
|
if (sc.Compare("NULL"))
|
|
{
|
|
AmmoNames.Push(NULL);
|
|
}
|
|
else
|
|
{
|
|
PClassAmmo *cls = dyn_cast<PClassAmmo>(PClass::FindClass(sc.String));
|
|
if (cls == NULL)
|
|
{
|
|
sc.ScriptError("Unknown ammo type '%s'", sc.String);
|
|
}
|
|
AmmoNames.Push(cls);
|
|
}
|
|
if (sc.CheckString("}")) break;
|
|
sc.MustGetStringName(",");
|
|
}
|
|
}
|
|
else if (sc.Compare("WeaponNames"))
|
|
{
|
|
WeaponNames.Clear(); // This won't be cleared by UnloadDEHSupp so we need to do it here explicitly
|
|
sc.MustGetStringName("{");
|
|
while (!sc.CheckString("}"))
|
|
{
|
|
sc.MustGetString();
|
|
PClass *cls = PClass::FindClass(sc.String);
|
|
if (cls == NULL || !cls->IsDescendantOf(RUNTIME_CLASS(AWeapon)))
|
|
{
|
|
sc.ScriptError("Unknown weapon type '%s'", sc.String);
|
|
}
|
|
WeaponNames.Push(static_cast<PClassActor *>(cls));
|
|
if (sc.CheckString("}")) break;
|
|
sc.MustGetStringName(",");
|
|
}
|
|
}
|
|
else if (sc.Compare("Aliases"))
|
|
{
|
|
sc.MustGetStringName("{");
|
|
while (!sc.CheckString("}"))
|
|
{
|
|
CodePointerAlias temp;
|
|
sc.MustGetString();
|
|
strncpy(temp.alias, sc.String, 19);
|
|
temp.alias[19]=0;
|
|
sc.MustGetStringName(",");
|
|
sc.MustGetString();
|
|
temp.name = sc.String;
|
|
sc.MustGetStringName(",");
|
|
sc.MustGetNumber();
|
|
temp.params = sc.Number;
|
|
MBFCodePointers.Push(temp);
|
|
if (sc.CheckString("}")) break;
|
|
sc.MustGetStringName(",");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
sc.ScriptError("Unknown section '%s'", sc.String);
|
|
}
|
|
|
|
sc.MustGetStringName(";");
|
|
}
|
|
return true;
|
|
}
|
|
catch(CRecoverableError &err)
|
|
{
|
|
// Don't abort if DEHSUPP loading fails.
|
|
// Just print the message and continue.
|
|
Printf("%s\n", err.GetMessage());
|
|
return false;
|
|
}
|
|
}
|
|
|
|
void FinishDehPatch ()
|
|
{
|
|
unsigned int touchedIndex;
|
|
|
|
for (touchedIndex = 0; touchedIndex < TouchedActors.Size(); ++touchedIndex)
|
|
{
|
|
PClassActor *type = TouchedActors[touchedIndex];
|
|
AActor *defaults1 = GetDefaultByType (type);
|
|
if (!(defaults1->flags & MF_SPECIAL))
|
|
{ // We only need to do this for pickups
|
|
continue;
|
|
}
|
|
|
|
// Create a new class that will serve as the actual pickup
|
|
char typeNameBuilder[32];
|
|
mysnprintf (typeNameBuilder, countof(typeNameBuilder), "DehackedPickup%d", touchedIndex);
|
|
PClassActor *subclass = static_cast<PClassActor *>(RUNTIME_CLASS(ADehackedPickup)->
|
|
CreateDerivedClass(typeNameBuilder, sizeof(ADehackedPickup)));
|
|
AActor *defaults2 = GetDefaultByType (subclass);
|
|
memcpy ((void *)defaults2, (void *)defaults1, sizeof(AActor));
|
|
|
|
// Make a copy of the replaced class's state labels
|
|
FStateDefinitions statedef;
|
|
statedef.MakeStateDefines(type);
|
|
|
|
if (!type->IsDescendantOf(RUNTIME_CLASS(AInventory)))
|
|
{
|
|
// If this is a hacked non-inventory item we must also copy AInventory's special states
|
|
statedef.AddStateDefines(RUNTIME_CLASS(AInventory)->StateList);
|
|
}
|
|
statedef.InstallStates(subclass, defaults2);
|
|
|
|
// Use the DECORATE replacement feature to redirect all spawns
|
|
// of the original class to the new one.
|
|
PClassActor *old_replacement = type->Replacement;
|
|
|
|
type->Replacement = subclass;
|
|
subclass->Replacee = type;
|
|
// If this actor was already replaced by another actor, copy that
|
|
// replacement over to this item.
|
|
if (old_replacement != NULL)
|
|
{
|
|
subclass->Replacement = old_replacement;
|
|
}
|
|
|
|
DPrintf ("%s replaces %s\n", subclass->TypeName.GetChars(), type->TypeName.GetChars());
|
|
}
|
|
|
|
// Now that all Dehacked patches have been processed, it's okay to free StateMap.
|
|
StateMap.Clear();
|
|
StateMap.ShrinkToFit();
|
|
TouchedActors.Clear();
|
|
TouchedActors.ShrinkToFit();
|
|
|
|
// Now it gets nasty: We have to fiddle around with the weapons' ammo use info to make Doom's original
|
|
// ammo consumption work as intended.
|
|
|
|
for(unsigned i = 0; i < WeaponNames.Size(); i++)
|
|
{
|
|
AWeapon *weap = (AWeapon*)GetDefaultByType(WeaponNames[i]);
|
|
bool found = false;
|
|
if (weap->flags6 & MF6_INTRYMOVE)
|
|
{
|
|
// Weapon sets an explicit amount of ammo to use so we won't need any special processing here
|
|
weap->flags6 &= ~MF6_INTRYMOVE;
|
|
}
|
|
else
|
|
{
|
|
weap->WeaponFlags |= WIF_DEHAMMO;
|
|
weap->AmmoUse1 = 0;
|
|
// to allow proper checks in CheckAmmo we have to find the first attack pointer in the Fire sequence
|
|
// and set its default ammo use as the weapon's AmmoUse1.
|
|
|
|
TMap<FState*, bool> StateVisited;
|
|
|
|
FState *state = WeaponNames[i]->FindState(NAME_Fire);
|
|
while (state != NULL)
|
|
{
|
|
bool *check = StateVisited.CheckKey(state);
|
|
if (check != NULL && *check)
|
|
{
|
|
break; // State has already been checked so we reached a loop
|
|
}
|
|
StateVisited[state] = true;
|
|
for(unsigned j = 0; AmmoPerAttacks[j].func != NULL; j++)
|
|
{
|
|
if (state->ActionFunc == *AmmoPerAttacks[j].func)
|
|
{
|
|
found = true;
|
|
int use = AmmoPerAttacks[j].ammocount;
|
|
if (use < 0) use = deh.BFGCells;
|
|
weap->AmmoUse1 = use;
|
|
break;
|
|
}
|
|
}
|
|
if (found) break;
|
|
state = state->GetNextState();
|
|
}
|
|
}
|
|
}
|
|
WeaponNames.Clear();
|
|
WeaponNames.ShrinkToFit();
|
|
}
|
|
|
|
void ModifyDropAmount(AInventory *inv, int dropamount);
|
|
|
|
bool ADehackedPickup::TryPickup (AActor *&toucher)
|
|
{
|
|
PClassActor *type = DetermineType ();
|
|
if (type == NULL)
|
|
{
|
|
return false;
|
|
}
|
|
RealPickup = static_cast<AInventory *>(Spawn (type, Pos(), NO_REPLACE));
|
|
if (RealPickup != NULL)
|
|
{
|
|
// The internally spawned item should never count towards statistics.
|
|
RealPickup->ClearCounters();
|
|
if (!(flags & MF_DROPPED))
|
|
{
|
|
RealPickup->flags &= ~MF_DROPPED;
|
|
}
|
|
// If this item has been dropped by a monster the
|
|
// amount of ammo this gives must be adjusted.
|
|
if (droppedbymonster)
|
|
{
|
|
ModifyDropAmount(RealPickup, 0);
|
|
}
|
|
if (!RealPickup->CallTryPickup (toucher))
|
|
{
|
|
RealPickup->Destroy ();
|
|
RealPickup = NULL;
|
|
return false;
|
|
}
|
|
GoAwayAndDie ();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
const char *ADehackedPickup::PickupMessage ()
|
|
{
|
|
if (RealPickup != nullptr)
|
|
return RealPickup->PickupMessage ();
|
|
else return "";
|
|
}
|
|
|
|
bool ADehackedPickup::ShouldStay ()
|
|
{
|
|
if (RealPickup != nullptr)
|
|
return RealPickup->ShouldStay ();
|
|
else return true;
|
|
}
|
|
|
|
bool ADehackedPickup::ShouldRespawn ()
|
|
{
|
|
if (RealPickup != nullptr)
|
|
return RealPickup->ShouldRespawn ();
|
|
else return false;
|
|
}
|
|
|
|
void ADehackedPickup::PlayPickupSound (AActor *toucher)
|
|
{
|
|
if (RealPickup != nullptr)
|
|
RealPickup->PlayPickupSound (toucher);
|
|
}
|
|
|
|
void ADehackedPickup::DoPickupSpecial (AActor *toucher)
|
|
{
|
|
Super::DoPickupSpecial (toucher);
|
|
// If the real pickup hasn't joined the toucher's inventory, make sure it
|
|
// doesn't stick around.
|
|
if (RealPickup != nullptr && RealPickup->Owner != toucher)
|
|
{
|
|
RealPickup->Destroy ();
|
|
}
|
|
RealPickup = nullptr;
|
|
}
|
|
|
|
void ADehackedPickup::Destroy ()
|
|
{
|
|
if (RealPickup != nullptr)
|
|
{
|
|
RealPickup->Destroy ();
|
|
RealPickup = nullptr;
|
|
}
|
|
Super::Destroy ();
|
|
}
|
|
|
|
PClassActor *ADehackedPickup::DetermineType ()
|
|
{
|
|
// Look at the actor's current sprite to determine what kind of
|
|
// item to pretend to me.
|
|
int min = 0;
|
|
int max = countof(DehSpriteMappings) - 1;
|
|
|
|
while (min <= max)
|
|
{
|
|
int mid = (min + max) / 2;
|
|
int lex = memcmp (DehSpriteMappings[mid].Sprite, sprites[sprite].name, 4);
|
|
if (lex == 0)
|
|
{
|
|
return PClass::FindActor(DehSpriteMappings[mid].ClassName);
|
|
}
|
|
else if (lex < 0)
|
|
{
|
|
min = mid + 1;
|
|
}
|
|
else
|
|
{
|
|
max = mid - 1;
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
void ADehackedPickup::Serialize(FArchive &arc)
|
|
{
|
|
Super::Serialize(arc);
|
|
arc << droppedbymonster;
|
|
}
|