gzdoom/src/thingdef/olddecorations.cpp
Christoph Oelckers d819aafcf3 - changed handling of duplicate classes in DECORATE.
Instead of replacing the original, the second class will get renamed now, using the originating file as an identifier. In the vast majority of cases this should do exactly what is needed: Create an unconflicting second class that can coexist with the original. Unless the class is used by name this should eliminate all problems with this, but so far I haven't seen anything that used them by name.

This is choosing the lesser of two evils. While some mod out there may get broken, the old setup meant that the first class of a given name could not be written out to a savegame because it was not retrievable when loading it back.
2016-10-07 15:28:25 +02:00

720 lines
19 KiB
C++

/*
** decorations.cpp
** Loads custom actors out of DECORATE lumps.
**
**---------------------------------------------------------------------------
** Copyright 2002-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.
**---------------------------------------------------------------------------
**
*/
// HEADER FILES ------------------------------------------------------------
#include "actor.h"
#include "info.h"
#include "sc_man.h"
#include "tarray.h"
#include "templates.h"
#include "r_defs.h"
#include "a_pickups.h"
#include "s_sound.h"
#include "cmdlib.h"
#include "p_lnspec.h"
#include "a_action.h"
#include "decallib.h"
#include "i_system.h"
#include "thingdef.h"
#include "thingdef_exp.h"
#include "r_data/r_translate.h"
// TYPES -------------------------------------------------------------------
struct FExtraInfo
{
char DeathSprite[5];
unsigned int SpawnStart, SpawnEnd;
unsigned int DeathStart, DeathEnd;
unsigned int IceDeathStart, IceDeathEnd;
unsigned int FireDeathStart, FireDeathEnd;
bool bSolidOnDeath, bSolidOnBurn;
bool bBurnAway, bDiesAway, bGenericIceDeath;
bool bExplosive;
double DeathHeight, BurnHeight;
};
class AFakeInventory : public AInventory
{
DECLARE_CLASS (AFakeInventory, AInventory);
public:
bool Respawnable;
bool ShouldRespawn ()
{
return Respawnable && Super::ShouldRespawn();
}
bool TryPickup (AActor *&toucher)
{
INTBOOL success = P_ExecuteSpecial(special, NULL, toucher, false,
args[0], args[1], args[2], args[3], args[4]);
if (success)
{
GoAwayAndDie ();
return true;
}
return false;
}
void DoPickupSpecial (AActor *toucher)
{
// The special was already executed by TryPickup, so do nothing here
}
};
IMPLEMENT_CLASS (AFakeInventory)
// PUBLIC FUNCTION PROTOTYPES ----------------------------------------------
// PRIVATE FUNCTION PROTOTYPES ---------------------------------------------
static void ParseInsideDecoration (Baggage &bag, AActor *defaults,
FExtraInfo &extra, EDefinitionType def, FScanner &sc, TArray<FState> &StateArray);
static void ParseSpriteFrames (PClassActor *info, TArray<FState> &states, FScanner &sc);
// PRIVATE DATA DEFINITIONS ------------------------------------------------
static const char *RenderStyles[] =
{
"STYLE_None",
"STYLE_Normal",
"STYLE_Fuzzy",
"STYLE_SoulTrans",
"STYLE_OptFuzzy",
"STYLE_Stencil",
"STYLE_Translucent",
"STYLE_Add",
//"STYLE_Shaded",
NULL
};
// CODE --------------------------------------------------------------------
//==========================================================================
//
//==========================================================================
DEFINE_CLASS_PROPERTY(respawns, 0, FakeInventory)
{
defaults->Respawnable = true;
}
//==========================================================================
//
// ParseOldDecoration
//
// Reads an old style decoration object
//
//==========================================================================
PClassActor *DecoDerivedClass(const FScriptPosition &sc, PClassActor *parent, FName typeName);
void ParseOldDecoration(FScanner &sc, EDefinitionType def)
{
Baggage bag;
TArray<FState> StateArray;
FExtraInfo extra;
PClassActor *type;
PClassActor *parent;
FName typeName;
parent = (def == DEF_Pickup) ? RUNTIME_CLASS(AFakeInventory) : RUNTIME_CLASS(AActor);
sc.MustGetString();
typeName = FName(sc.String);
type = DecoDerivedClass(FScriptPosition(sc), parent, typeName);
ResetBaggage(&bag, parent);
bag.Info = type;
#ifdef _DEBUG
bag.ClassName = type->TypeName;
#endif
type->GameFilter = GAME_Any;
sc.MustGetStringName("{");
memset (&extra, 0, sizeof(extra));
ParseInsideDecoration (bag, (AActor *)(type->Defaults), extra, def, sc, StateArray);
bag.Info->NumOwnedStates = StateArray.Size();
if (bag.Info->NumOwnedStates == 0)
{
sc.ScriptError ("%s does not define any animation frames", typeName.GetChars() );
}
else if (extra.SpawnEnd == 0)
{
sc.ScriptError ("%s does not have a Frames definition", typeName.GetChars() );
}
else if (def == DEF_BreakableDecoration && extra.DeathEnd == 0)
{
sc.ScriptError ("%s does not have a DeathFrames definition", typeName.GetChars() );
}
else if (extra.IceDeathEnd != 0 && extra.bGenericIceDeath)
{
sc.ScriptError ("You cannot use IceDeathFrames and GenericIceDeath together");
}
if (extra.IceDeathEnd != 0)
{
// Make a copy of the final frozen frame for A_FreezeDeathChunks
FState icecopy = StateArray[extra.IceDeathEnd-1];
StateArray.Push (icecopy);
type->NumOwnedStates += 1;
}
type->OwnedStates = new FState[type->NumOwnedStates];
memcpy (type->OwnedStates, &StateArray[0], type->NumOwnedStates * sizeof(type->OwnedStates[0]));
if (type->NumOwnedStates == 1)
{
type->OwnedStates->Tics = -1;
type->OwnedStates->TicRange = 0;
type->OwnedStates->Misc1 = 0;
}
else
{
size_t i;
// Spawn states loop endlessly
for (i = extra.SpawnStart; i < extra.SpawnEnd-1; ++i)
{
type->OwnedStates[i].NextState = &type->OwnedStates[i+1];
}
type->OwnedStates[i].NextState = &type->OwnedStates[extra.SpawnStart];
// Death states are one-shot and freeze on the final state
if (extra.DeathEnd != 0)
{
for (i = extra.DeathStart; i < extra.DeathEnd-1; ++i)
{
type->OwnedStates[i].NextState = &type->OwnedStates[i+1];
}
if (extra.bDiesAway || def == DEF_Projectile)
{
type->OwnedStates[i].NextState = NULL;
}
else
{
type->OwnedStates[i].Tics = -1;
type->OwnedStates[i].TicRange = 0;
type->OwnedStates[i].Misc1 = 0;
}
if (def == DEF_Projectile)
{
if (extra.bExplosive)
{
type->OwnedStates[extra.DeathStart].SetAction("A_Explode");
}
}
else
{
// The first frame plays the death sound and
// the second frame makes it nonsolid.
type->OwnedStates[extra.DeathStart].SetAction("A_Scream");
if (extra.bSolidOnDeath)
{
}
else if (extra.DeathStart + 1 < extra.DeathEnd)
{
type->OwnedStates[extra.DeathStart+1].SetAction("A_NoBlocking");
}
else
{
type->OwnedStates[extra.DeathStart].SetAction("A_ScreamAndUnblock");
}
if (extra.DeathHeight == 0)
{
extra.DeathHeight = ((AActor*)(type->Defaults))->Height;
}
type->DeathHeight = extra.DeathHeight;
}
bag.statedef.SetStateLabel("Death", &type->OwnedStates[extra.DeathStart]);
}
// Burn states are the same as death states, except they can optionally terminate
if (extra.FireDeathEnd != 0)
{
for (i = extra.FireDeathStart; i < extra.FireDeathEnd-1; ++i)
{
type->OwnedStates[i].NextState = &type->OwnedStates[i+1];
}
if (extra.bBurnAway)
{
type->OwnedStates[i].NextState = NULL;
}
else
{
type->OwnedStates[i].Tics = -1;
type->OwnedStates[i].TicRange = 0;
type->OwnedStates[i].Misc1 = 0;
}
// The first frame plays the burn sound and
// the second frame makes it nonsolid.
type->OwnedStates[extra.FireDeathStart].SetAction("A_ActiveSound");
if (extra.bSolidOnBurn)
{
}
else if (extra.FireDeathStart + 1 < extra.FireDeathEnd)
{
type->OwnedStates[extra.FireDeathStart+1].SetAction("A_NoBlocking");
}
else
{
type->OwnedStates[extra.FireDeathStart].SetAction("A_ActiveAndUnblock");
}
if (extra.BurnHeight == 0) extra.BurnHeight = ((AActor*)(type->Defaults))->Height;
type->BurnHeight = extra.BurnHeight;
bag.statedef.SetStateLabel("Burn", &type->OwnedStates[extra.FireDeathStart]);
}
// Ice states are similar to burn and death, except their final frame enters
// a loop that eventually causes them to bust to pieces.
if (extra.IceDeathEnd != 0)
{
for (i = extra.IceDeathStart; i < extra.IceDeathEnd-1; ++i)
{
type->OwnedStates[i].NextState = &type->OwnedStates[i+1];
}
type->OwnedStates[i].NextState = &type->OwnedStates[type->NumOwnedStates-1];
type->OwnedStates[i].Tics = 5;
type->OwnedStates[i].TicRange = 0;
type->OwnedStates[i].Misc1 = 0;
type->OwnedStates[i].SetAction("A_FreezeDeath");
i = type->NumOwnedStates - 1;
type->OwnedStates[i].NextState = &type->OwnedStates[i];
type->OwnedStates[i].Tics = 1;
type->OwnedStates[i].TicRange = 0;
type->OwnedStates[i].Misc1 = 0;
type->OwnedStates[i].SetAction("A_FreezeDeathChunks");
bag.statedef.SetStateLabel("Ice", &type->OwnedStates[extra.IceDeathStart]);
}
else if (extra.bGenericIceDeath)
{
bag.statedef.SetStateLabel("Ice", RUNTIME_CLASS(AActor)->FindState(NAME_GenericFreezeDeath));
}
}
if (def == DEF_BreakableDecoration)
{
((AActor *)(type->Defaults))->flags |= MF_SHOOTABLE;
}
if (def == DEF_Projectile)
{
((AActor *)(type->Defaults))->flags |= MF_DROPOFF|MF_MISSILE;
}
bag.statedef.SetStateLabel("Spawn", &type->OwnedStates[extra.SpawnStart]);
bag.statedef.InstallStates (type, ((AActor *)(type->Defaults)));
}
//==========================================================================
//
// ParseInsideDecoration
//
// Parses attributes out of a definition terminated with a }
//
//==========================================================================
static void ParseInsideDecoration (Baggage &bag, AActor *defaults,
FExtraInfo &extra, EDefinitionType def, FScanner &sc, TArray<FState> &StateArray)
{
AFakeInventory *const inv = static_cast<AFakeInventory *>(defaults);
char sprite[5] = "TNT1";
sc.MustGetString ();
while (!sc.Compare ("}"))
{
if (sc.Compare ("DoomEdNum"))
{
sc.MustGetNumber ();
if (sc.Number < -1 || sc.Number > 32767)
{
sc.ScriptError ("DoomEdNum must be in the range [-1,32767]");
}
bag.Info->DoomEdNum = (SWORD)sc.Number;
}
else if (sc.Compare ("SpawnNum"))
{
sc.MustGetNumber ();
if (sc.Number < 0 || sc.Number > 255)
{
sc.ScriptError ("SpawnNum must be in the range [0,255]");
}
bag.Info->SpawnID = (BYTE)sc.Number;
}
else if (sc.Compare ("Sprite") || (
(def == DEF_BreakableDecoration || def == DEF_Projectile) &&
sc.Compare ("DeathSprite") &&
(extra.DeathSprite[0] = 1)) // This is intentionally = and not ==
)
{
sc.MustGetString ();
if (sc.StringLen != 4)
{
sc.ScriptError ("Sprite name must be exactly four characters long");
}
if (extra.DeathSprite[0] == 1)
{
memcpy (extra.DeathSprite, sc.String, 4);
}
else
{
memcpy (sprite, sc.String, 4);
}
}
else if (sc.Compare ("Frames"))
{
sc.MustGetString ();
extra.SpawnStart = StateArray.Size();
ParseSpriteFrames (bag.Info, StateArray, sc);
extra.SpawnEnd = StateArray.Size();
}
else if ((def == DEF_BreakableDecoration || def == DEF_Projectile) &&
sc.Compare ("DeathFrames"))
{
sc.MustGetString ();
extra.DeathStart = StateArray.Size();
ParseSpriteFrames (bag.Info, StateArray, sc);
extra.DeathEnd = StateArray.Size();
}
else if (def == DEF_BreakableDecoration && sc.Compare ("IceDeathFrames"))
{
sc.MustGetString ();
extra.IceDeathStart = StateArray.Size();
ParseSpriteFrames (bag.Info, StateArray, sc);
extra.IceDeathEnd = StateArray.Size();
}
else if (def == DEF_BreakableDecoration && sc.Compare ("BurnDeathFrames"))
{
sc.MustGetString ();
extra.FireDeathStart = StateArray.Size();
ParseSpriteFrames (bag.Info, StateArray, sc);
extra.FireDeathEnd = StateArray.Size();
}
else if (def == DEF_BreakableDecoration && sc.Compare ("GenericIceDeath"))
{
extra.bGenericIceDeath = true;
}
else if (def == DEF_BreakableDecoration && sc.Compare ("BurnsAway"))
{
extra.bBurnAway = true;
}
else if (def == DEF_BreakableDecoration && sc.Compare ("DiesAway"))
{
extra.bDiesAway = true;
}
else if (sc.Compare ("Alpha"))
{
sc.MustGetFloat ();
defaults->Alpha = clamp (sc.Float, 0.0, 1.0);
}
else if (sc.Compare ("Scale"))
{
sc.MustGetFloat ();
defaults->Scale.X = defaults->Scale.Y = sc.Float;
}
else if (sc.Compare ("RenderStyle"))
{
sc.MustGetString ();
defaults->RenderStyle = LegacyRenderStyles[sc.MustMatchString (RenderStyles)];
}
else if (sc.Compare ("Radius"))
{
sc.MustGetFloat ();
defaults->radius = sc.Float;
}
else if (sc.Compare ("Height"))
{
sc.MustGetFloat ();
defaults->Height = sc.Float;
}
else if (def == DEF_BreakableDecoration && sc.Compare ("DeathHeight"))
{
sc.MustGetFloat ();
extra.DeathHeight = sc.Float;
}
else if (def == DEF_BreakableDecoration && sc.Compare ("BurnHeight"))
{
sc.MustGetFloat ();
extra.BurnHeight = sc.Float;
}
else if (def == DEF_BreakableDecoration && sc.Compare ("Health"))
{
sc.MustGetNumber ();
defaults->health = sc.Number;
}
else if (def == DEF_Projectile && sc.Compare ("ExplosionRadius"))
{
sc.MustGetNumber ();
bag.Info->ExplosionRadius = sc.Number;
extra.bExplosive = true;
}
else if (def == DEF_Projectile && sc.Compare ("ExplosionDamage"))
{
sc.MustGetNumber ();
bag.Info->ExplosionDamage = sc.Number;
extra.bExplosive = true;
}
else if (def == DEF_Projectile && sc.Compare ("DoNotHurtShooter"))
{
bag.Info->DontHurtShooter = true;
}
else if (def == DEF_Projectile && sc.Compare ("Damage"))
{
sc.MustGetNumber ();
defaults->SetDamage(sc.Number);
}
else if (def == DEF_Projectile && sc.Compare ("DamageType"))
{
sc.MustGetString ();
if (sc.Compare ("Normal"))
{
defaults->DamageType = NAME_None;
}
else
{
defaults->DamageType = sc.String;
}
}
else if (def == DEF_Projectile && sc.Compare ("Speed"))
{
sc.MustGetFloat ();
defaults->Speed = sc.Float;
}
else if (sc.Compare ("Mass"))
{
sc.MustGetFloat ();
defaults->Mass = SDWORD(sc.Float);
}
else if (sc.Compare ("Translation1"))
{
sc.MustGetNumber ();
if (sc.Number < 0 || sc.Number > 2)
{
sc.ScriptError ("Translation1 must be in the range [0,2]");
}
defaults->Translation = TRANSLATION(TRANSLATION_Standard, sc.Number);
}
else if (sc.Compare ("Translation2"))
{
sc.MustGetNumber ();
if (sc.Number < 0 || sc.Number >= MAX_ACS_TRANSLATIONS)
{
#define ERROR(foo) "Translation2 must be in the range [0," #foo "]"
sc.ScriptError (ERROR(MAX_ACS_TRANSLATIONS));
#undef ERROR
}
defaults->Translation = TRANSLATION(TRANSLATION_LevelScripted, sc.Number);
}
else if ((def == DEF_BreakableDecoration || def == DEF_Projectile) &&
sc.Compare ("DeathSound"))
{
sc.MustGetString ();
defaults->DeathSound = sc.String;
}
else if (def == DEF_BreakableDecoration && sc.Compare ("BurnDeathSound"))
{
sc.MustGetString ();
defaults->ActiveSound = sc.String;
}
else if (def == DEF_Projectile && sc.Compare ("SpawnSound"))
{
sc.MustGetString ();
defaults->SeeSound = sc.String;
}
else if (def == DEF_Projectile && sc.Compare ("DoomBounce"))
{
defaults->BounceFlags = BOUNCE_DoomCompat;
}
else if (def == DEF_Projectile && sc.Compare ("HereticBounce"))
{
defaults->BounceFlags = BOUNCE_HereticCompat;
}
else if (def == DEF_Projectile && sc.Compare ("HexenBounce"))
{
defaults->BounceFlags = BOUNCE_HexenCompat;
}
else if (def == DEF_Pickup && sc.Compare ("PickupSound"))
{
sc.MustGetString ();
inv->PickupSound = sc.String;
}
else if (def == DEF_Pickup && sc.Compare ("PickupMessage"))
{
sc.MustGetString ();
static_cast<PClassInventory *>(bag.Info)->PickupMessage = sc.String;
}
else if (def == DEF_Pickup && sc.Compare ("Respawns"))
{
inv->Respawnable = true;
}
else if (def == DEF_BreakableDecoration && sc.Compare ("SolidOnDeath"))
{
extra.bSolidOnDeath = true;
}
else if (def == DEF_BreakableDecoration && sc.Compare ("SolidOnBurn"))
{
extra.bSolidOnBurn = true;
}
else if (sc.String[0] != '*')
{
HandleActorFlag(sc, bag, sc.String, NULL, '+');
}
else
{
sc.ScriptError (NULL);
}
sc.MustGetString ();
}
unsigned int i;
int spr = GetSpriteIndex(sprite);
for (i = 0; i < StateArray.Size(); ++i)
{
StateArray[i].sprite = spr;
}
if (extra.DeathSprite[0] && extra.DeathEnd != 0)
{
int spr = GetSpriteIndex(extra.DeathSprite);
for (i = extra.DeathStart; i < extra.DeathEnd; ++i)
{
StateArray[i].sprite = spr;
}
}
}
//==========================================================================
//
// ParseSpriteFrames
//
// Parses a sprite-based animation sequence out of a decoration definition.
// You can have multiple sequences, and they will be appended together.
// A sequence definition looks like this:
//
// "<rate>:<frames>,<rate>:<frames>,<rate>:<frames>,..."
//
// Rate is a number describing the number of tics between frames in this
// sequence. If you don't specify it, then a rate of 4 is used. Frames is
// a list of consecutive frame characters. Each frame can be postfixed with
// the * character to indicate that it is fullbright.
//
// Examples:
// ShortRedTorch looks like this:
// "ABCD"
//
// HeadCandles looks like this:
// "6:AB"
//
// TechLamp looks like this:
// "A*B*C*D*"
//
// BloodyTwich looks like this:
// "10:A, 15:B, 8:C, 6:B"
//==========================================================================
static void ParseSpriteFrames (PClassActor *info, TArray<FState> &states, FScanner &sc)
{
FState state;
char *token = strtok (sc.String, ",\t\n\r");
memset (&state, 0, sizeof(state));
while (token != NULL)
{
// Skip leading white space
while (*token == ' ')
token++;
int rate = 4;
bool firstState = true;
char *colon = strchr (token, ':');
if (colon != NULL)
{
char *stop;
*colon = 0;
rate = strtol (token, &stop, 10);
if (stop == token || rate < 1 || rate > 65534)
{
sc.ScriptError ("Rates must be in the range [0,65534]");
}
token = colon + 1;
}
state.Tics = rate;
state.TicRange = 0;
while (*token)
{
if (*token == ' ')
{
}
else if (*token == '*')
{
if (firstState)
{
sc.ScriptError ("* must come after a frame");
}
state.Fullbright = true;
}
else if (*token < 'A' || *token > ']')
{
sc.ScriptError ("Frames must be A-Z, [, \\, or ]");
}
else
{
if (!firstState)
{
states.Push (state);
}
firstState = false;
state.Frame = *token-'A';
}
++token;
}
if (!firstState)
{
states.Push (state);
}
token = strtok (NULL, ",\t\n\r");
}
}